React中封装组件的一些方法

发表于 3年以前  | 总阅读数:631 次

最近参与了一个基于Raact技术栈的项目,距离我上一次在工作中react已经过去了挺长一段时间,因此打算整理在react中封装组件的一些方法。

1、extends 正向继承

对于类组件而言,可以通过extends继承某个父类,从而获得一些公共的能力

class LogPage extends React.Component {
    trackLog() {
        console.log("trackLog");
    }
}

class Page1 extends LogPage {
    onBtnClick = () => {
        console.log('click')
        this.trackLog();
    };

    render() {
        return <button onClick={this.onBtnClick}>click</button>;
    }
}

借助OOP的思想,可以通过封装、继承和多态来实现数据的隔离和功能的复用。

2、HOC

高阶组件其实就是参数为组件,返回值为新组件的函数

高阶组件是React中比较常见的一种做法,主要用于增强某些组件的功能,封装一些公共操作,如处理埋点日志、执行公共逻辑、渲染公共UI等。

2.1. 劫持props

高阶组件会返回一个新的组件,这个组件会拦截传递过来的props,这样就可以做一些特殊的处理,或者仅仅是添加一些通用的props

function HOC(Comp) {
  const commonProps = { x: 1, commonMethod1, commonMethod2  };
  return props => <Comp {...commonProps} {...props} />;
}

看起来像组件注入一些通用的props就更轻松了。

2.2. 反向继承

高阶组件的核心思想是返回一个新的组件,如果是类组件,甚至可以通过继承的方式劫持组件原本的生命周期函数,扩展新的功能

function HOC(Comp){
  return class SubComp extends Comp {
    componentDidMount(){
        // 处理新的生命周期方法,可以按需要决定是否调用supder.componentDidMount
    }

    render(){
        // 使用原始的render
      return super.render();
    }
  }
}

2.3. 控制渲染

比如我们需要判断某个页面是否需要登录,一种做法是直接在页面组件逻辑中编写判断,如果存在多个这种的页面,就变得重复了

利用HOC可以方便地处理这些逻辑。

export default (Comp) => (props) => {
  const isLogin = checkLogin()
  if (isLogin) {
    return (<Comp {...props}/>)
  }
  return (<Redirect to={{ pathname: 'login' }}/>)
}

对于被包裹的组件而言,HOC更像是一个装饰器添加额外的功能;而对于需要处理多个组件的开发者而言,HOC是一种封装公共逻辑的方案

2.4. HOC的缺点

劫持Props是HOC最常用的功能之一,但这也是它的缺点:层级的嵌套和状态的透传。

对于HOC本身而言,传递给他的props是不需要关心的,他只是负责将props透传下去。这就要求对于一些特殊的prop如ref等,需要额外使用forwardRef才能够满足需求。

此外,我认为这也导致组件的props来源变得不清晰。最后组件经过多个HOC的装饰之后,我们就很难区分某个props注入的数据到底是哪里来的了

3、 Render Props

在某些场景下,对于组件调用方而言,希望组件能够提供一些自定义的渲染功能,与vue的slot类似

3.1. prop传递ReactElement

React组件默认的prop: children可以实现default slot的功能

const Foo = ({ children }) => {
    return (
        <div>
            <h1>Foo</h1>
            {children}
        </div>
    );
};

<Foo>hello from parent</Foo>

在jsX解析的时候,会将组件内的内容解析为React Element,然后作为children属性传递给组件。基于“prop可以传递ReactElement”这个思路,可以实现很多骚操作。

const Bar = ({ head, body }) => {
    return (
        <div>
            <h1>{head}</h1>
            <p>{body}</p>
        </div>
    );
};

<Bar 
  head={<span>title</span>} 
  body={<span>body</span>}></Bar>

类似于vue的具名插槽,使用起来却更加直观,这就是jsX灵活而强大的表现。

3.2. prop传递函数

但这种直接传递ReactElement也存在一些问题,那就是这些节点都是在父元素定义的。

如果能够根据组件内部的一些数据来动态渲染要展示的元素,这样就会更加灵活了。换言之,我们需要实现在组件内部动态构建渲染元素。

最简单的解决办法就是传递一个函数,由组件内部通过传参的形式通过函数动态生成需要渲染的元素。

const Baz = ({ renderHead }) => {
    const count = 123;
    return <div>{renderHead(count)}</div>;
};

<Baz 
  renderHead={(count) => <span>count is {count}</span>}
  ></Baz>

通过函数的方式,可以在不改动组件内部实现的前提下,利用组件的数据实现UI分发和逻辑复用,类似于Vue的插槽作用域,也跟JavaScript中常见的回调函数作用一致。

React官方把这种技术称作Render Props:

Render Props是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术

Render Props有下面几个特点

  • 也是一个prop,用于父子组件之间传递数据
  • 他的值是一个函数,其参数由子组件在合适的时候传入
  • 通常用来render(渲染)某个元素或组件

再举一个更常用的例子,渲染列表组件,

const DealItem = ({ item }) => {
    return (
        <li>
            <p>{item.name}</p>
        </li>
    );
};

实现了列表单个元素组件之后,就可以Array.prototype.map一把梭渲染列表。

const DealListDemo = () => {
    const list = [{ name: "1" }, { name: "2" }];
    return (
        <ul>
            {list.map((item, index) => {
                return <DealItem key={index} item={item}></DealItem>;
            })}
        </ul>
    );
};

如果需要渲染多个类似的列表,如DealItem2、DealItem3之类的,这个时候就可以把重复的list.map解耦出来,实现一个纯粹的List组件。

// 这里假设List组件会执行一些渲染列表的公共逻辑,如滚动加载、窗口列表啥的
const List = ({ list, children }) => {
    return (
        <ul>
            {list.map((item, index) => {
                return children(item, index);
            })}
        </ul>
    );
};

然后通过Render Props就可以动态渲染不同的元素组件列表了。

const DealListDemo = () => {
    const list = [{ name: "1" }, { name: "2" }];
    return (
        <List list={list}>
            {(item, index) => <DealItem key={index} item={item}></DealItem>}
        </List>
    );
};
// 渲染不同的组件元素,只需要提供新的元素组件即可
const DealListDemo2 = () => {
    const list = [{ name: "1" }, { name: "2" }];
    return (
        <List list={list}>
            {(item, index) => <DealItem2 key={index} item={item}></DealItem>}
        </List>
    );
};

3.3. prop传递组件

上面提到Render props是值为函数的prop,这个函数返回的是ReactElement。那不就是一个函数组件吗?既然如此,是不是也可以直接传递组件呢?答案是肯定的。

比如现在需要实现一个toolTip组件,可以在某些场景下切换弹窗。

const Modal = ({ Trigger }) => {
    const [visible, setVisible] = useState(false);

    return (
        <div>
            <Trigger
                toggle={() => {
                    setVisible(!visible);
                }}
            />
            <dialog open={visible}>
                <p>tooltip</p>
            </dialog>
        </div>
    );
};

现在就可以很轻易的实现一些可以触发tool的组件,但实现了和Modal的完全分离。

const ModalButton = ({ toggle }) => {
    return <button onClick={toggle}>click</button>;
};

// 当点击该按钮时会切换弹窗
<Modal Trigger={ModalButton}></Modal>

const ModalTitle = ({ toggle }) => {
    return <h1 onClick={toggle}>click</h1>;
};

// 点击标题时会切换弹窗
<Modal Trigger={ModalTitle}></Modal>

上面这种并不是使用Render props的常规方式,但也展示了利用prop实现UI扩展的一些特殊用法

3.4. Render Props存在的问题

Render Props可以有效地以松散耦合的方式设计组件,但由于其本质是一个函数,也会存在回调嵌套过深的问题:当返回的节点也需要传入render props时,就会发生多层嵌套。

<Demo1>
  {(props1)=>{
    return (
      <Demo2>
        {(props2)=>{
          return (<span>{props1}, {props2}</span>)
        }}
      </Demo2>)
  }}
</Demo1>

一种解决办法是使用react-adopt,它提供了组合多个render props返回结果的功能。

4、Hooks

强烈建议阅读官方文档,比我自己写的好得多。

4.1. Hooks解决的问题

React中组件分为了函数组件和Class组件,函数组件是无状态的,在Hooks之前,只能通过props控制函数组件的数据,如果希望实现一个带状态的组件,则需要通过Class组件的instace来维护。

Class组件主要有几个问题

  • 逻辑分散,相互关连的逻辑分散在各个生命周期函数;每个生命周期函数又塞满了各不相同的逻辑
  • 逻辑复用需要通过高阶组件HOC或者Render Props来处理,

不论是HOC还是Render Props,都需要重新组织组件结构,很容易形成组件嵌套,代码阅读性和可维护性都会变差。

因此需要一种扁平化的逻辑复用的方式,因此Hooks出现了。其优点有

  • 扁平化的逻辑复用,在无需修改组件结构的情况下复用状态逻辑
  • 将相互关联的部分放在一起,互不相关的地方相互隔离
  • 函数式编程

本章节将介绍Hook常见的使用方式及注意事项

4.2. useState 初始值

useState传入的初始化值只有首次会生效,这在使用props传入值作为初始化值可能会带来一些误导

下面实现一个复选框组件。

const Checkbox = ({ initChecked }) => {
    const [checked, setChecked] = useState(initChecked);

    const toggleChecked = () => {
        setChecked(!checked);
    };

    return (
        <label>
            <input type="checkbox" checked={checked} onChange={toggleChecked} />{" "}
            {checked ? "checked" : "unchecked"}
        </label>
    );
};

假设传入的props发生了变化。

const HookDemo = () => {
    const [checked, setChecked] = useState(false);
    useEffect(()=>{
        // 假设这里请求了接口获取返回值,用来初始化默认的checked
        setTimeout(()=>{
            setChecked(true)
        },1000)
      }, [])

    return (
        <div>
            <Checkbox initChecked={checked}></Checkbox>
        </div>
    );
};

此时修改了props initChecked的值,但Checkbox组件本身却不会更新。如果希望当props更新时继续修改Checkbox的选中状态,可以借助useEffect。

const Checkbox = ({ initChecked }) => {
        // ...
    useEffect(() => {
        setChecked(initChecked);
    }, [initChecked]);
    // ...
};

这个写法类型于Vue的自定义model写法

export default {
    props: {
        initChecked: {
      type: Boolean,
    }
    },
  data(){
    return {
      checked: this.initChecked
    }
  },
  watch:{
    initChecked(newVal){
      this.checked = newValue
    },
    checked(newVal){
      this.$emit('input', newVal)
    }
  }
}

4.3. 闭包陷阱

初使用Hooks时,比较常见的一个错误就是闭包。

下面实现了一个定时器组件,在挂载时开启定时器,每秒更新数值。

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

    useEffect(() => {
        let timer = setInterval(() => {
            setCount(count + 1);
        }, 1000);
        return ()=>{
            clearInterval(timer)
        }
    }, []);
    return <div>{count}</div>;
};

事实上每次更新之后count的值都不会变化,其原因跟

for (var i = 0; i < 10; ++i) {
    setTimeout(function () {
        console.log(i);
    }, 1000);
}

最后会打印出10个5的原因一样,

定时器在首次渲染的时候注册,后续的更新不会再修改定时器,因此其回调函数的作用域内的自由变量count,始终都是首次渲染时的值,不会发生改变。

一种解决办法是使用函数式的setCount,可以获取到最新的count值。

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

    useEffect(() => {
        let timer = setInterval(() => {
            setCount((c) => c + 1); // 可以拿到上一轮的值
        }, 1000);
        return () => {
            clearInterval(timer);
        };
    }, []);
    return <div>{count}</div>;
};

但如果是想在定时器回调函数中先根据上一轮的值进行一些处理,这种方法就无能为力了

归根到底是想在多次渲染之间保存一个值,最简单的做法是使用外部自由变量来保存。

let globalCount = 0
const IntervalDemo2 = () => {
    const [count, setCount] = useState(0);

    useEffect(() => {
        let timer = setInterval(() => {
            globalCount++
            console.log(globalCount)
            setCount(globalCount);
        }, 1000);

        return () => {
            clearInterval(timer);
        };
    }, []);
    return <div>{count}</div>;
};

当然这种方式肯定是存在问题的,在组件被重复或继续使用时,外部的globalCount会被污染。

要想在多次渲染期间共享同一个变量,官方的做法是使用useRef。

const IntervalDemo3 = () => {
    const [count, setCount] = useState(0);
    const countRef = useRef(0);

    useEffect(() => {
        let timer = setInterval(() => {
            countRef.current += 1;
            setCount(countRef.current);
        }, 1000);

        return () => {
            clearInterval(timer);
        };
    }, []);
    return <div>{count}</div>;
};

4.4. render Hook

在Class组件的使用中,在某些场景下可能期望获取组件的实例,方便调用组件上面的一些方法,最经典的场景是调用Form.validate()表单组件的字段校验。

class Form extends React.Component {
    validate = () => {
        console.log("validate form");
    };
    render() {
        return <div>form</div>;
    }
}

可以通过ref获取组件实例然后调用组件方法。

const Parent = () => {
    const ref = useRef(null)
    useEffect(()=>{
        const instance = ref.current
        instance.validate()
    },[])
    return (
      <Form ref={ref}></Form>
    );
};

在函数组件中,并不存在组件instance这一说法,也无法直接设置ref属性,直接在函数组件上使用ref会出现警告

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

为了实现与类组件的功能,需要使用借助forwardRef和useImperativeHandle

const Form2 = forwardRef((props, ref)=>{
      // 实现ref获取到实例相关的接口
    useImperativeHandle(ref, ()=>{
        return {
            validate(){
                console.log('validate')
            }
        }
    })
    return (<div>form</div>)
})

上面这种通过ref调用接口的操作,其思路都是先拿到组件实例,然后再进行操作。

但是现在有了Hook,我们可以将组件和操作组件的方法通过hook暴露出来,无需再通过ref了。

const useForm = () => {
    const validate = () => {
        console.log("validate form");
    };
    const render = () => {
        return <div>form</div>;
    };

    return {
        render,
        validate,
    };
};

const FormDemo = ()=>{
    const {render, validate} = useForm()
    useEffect(() => {
        validate()
    }, []);

    return render()
}

相较于ref获取类组件实例,这种实现看起来更加简单清晰,一切皆是函数。

借助这种包含渲染render功能的hook和JSX的强大表现力,可以实现很多有趣的组件,如弹窗。

一般的全局弹窗组件是通过ReactDOM.render将弹窗组件渲染到body节点上,然后使用Modal.info等全局接口展示。

这种写法的好处是灵活,缺点也很明显,无法与当前应用共享同一个context,参考antd Modal FAQ。

借助render hook的思路,可以通过一种取巧的方式实现。

const Modal = ({ visible, children }) => {
    return <dialog open={visible}>{children}</dialog>;
};

const useModal = (content) => {
    const [visible, setVisible] = useState(false);
    const modal = <Modal visible={visible}>{content}</Modal>;

    const toggleModal = () => {
        setVisible(!visible);
    };
    return {
        modal,
        toggleModal,
    };
};

使用起来很方便。

const ModalDemo = () => {
    const { modal, toggleModal } = useModal(<h1>hi model</h1>);

    return (
        <div>
            {modal}
            <button onClick={toggleModal}>toggle</button>
        </div>
    );
};

由于返回的ReactElement也是渲染在当前组件树中,因此就不存在context丢失的问题。

5、小结

本文主要总结了几种封装React组件的方式,包括正向继承、HOC、Render Props、 Hooks等方式,每种方式都有各自的优缺点。恰好最近参与了新的React项目,可以多尝试一下这些方法。

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/Zq1-XLHuh6-edGcTmPojjQ

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录