[NervJS/taro]taro 能支持 react-dom 中的 createPortal 吗,或实现类似api

2024-07-15 411 views
2
这个特性解决了什么问题?

一些组件更适用于适用createPortal在其他节点生成

这个 API ### 长什么样?

createPortal(<></>, otherNode)

回答

4

+1。

5

请问有计划支持吗 @Chen-jj

3

所以现在是怎样,实现了吗,有计划吗

4

目前是自己实现了一个,原理就是每个页面包一个container,然后通过useReducer、createContext将portal组件包裹的内容渲染到container里

1

我参考 ant-design-mobile 的 https://github.com/ant-design/ant-design-mobile-rn/tree/4344e2850727a3fa1c1f7691f362438e2a3a6bfc/components/portal 实现了一个 Portal 组件。

Portal.tsx

import { PropsWithChildren, useContext, useEffect, useState } from "react";
import PortalProvider, { PortalContext } from "./PortalProvider";
import PortalSlot from "./PortalSlot";

const Portal = ({ children }: PropsWithChildren<{}>) => {
  const manage = useContext(PortalContext);
  const [key, setKey] = useState(0);

  // 获取 key
  useEffect(() => {
    if (manage?.getKey) {
      if (!key) {
        setKey(manage.getKey());
      }
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [manage?.getKey]);

  // 挂载内容
  useEffect(() => {
    if (key) {
      manage?.mount(key, children);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [children, key]);

  // 挂载内容
  useEffect(() => {
    return () => {
      if (key) {
        manage?.umount(key);
      }
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [key]);

  return <></>;
};

Portal.Provider = PortalProvider;
Portal.Slot = PortalSlot;

export default Portal;

PortalSlot.tsx

import { PortalContext } from "./PortalProvider";

const PortalSlot = () => {
  return (
    <PortalContext.Consumer>
      {manage => {
        return (
          <>
            {/* 展示挂载过来的 portal */}
            {Object.keys(manage?.portalChildren || {}).map(key => {
              return manage?.portalChildren[key];
            })}
          </>
        );
      }}
    </PortalContext.Consumer>
  );
};

export default PortalSlot;

PortalProvider.tsx

import React, {
  PropsWithChildren,
  useCallback,
  useMemo,
  useRef,
  useState
} from "react";

interface PortalChildren {
  [key: number]: React.ReactNode;
}

interface Manage {
  getKey: () => number;
  mount: (key: number, children: React.ReactNode) => void;
  umount: (key: number) => void;
  portalChildren: PortalChildren;
}

export const PortalContext = React.createContext<Manage | undefined>(undefined);

// 提供将 children 挂载到指定位置的能力,配合 PortalSlot 实现
const PortalProvider = ({ children }: PropsWithChildren<{}>) => {
  const nextKey = useRef(0);
  const [portalChildren, setPortalChildren] = useState<PortalChildren>({});

  const mount = useCallback((key: number, c: React.ReactNode) => {
    setPortalChildren(currentPortalChildren => {
      currentPortalChildren[key] = c;
      return { ...currentPortalChildren };
    });
  }, []);

  const umount = useCallback((key: number) => {
    setPortalChildren(currentPortalChildren => {
      delete currentPortalChildren[key];
      return { ...currentPortalChildren };
    });
  }, []);

  const getKey = useCallback(() => {
    nextKey.current++;
    return nextKey.current;
  }, []);

  const manage: Manage = useMemo(
    () => ({
      getKey,
      mount,
      umount,
      portalChildren
    }),
    [getKey, mount, portalChildren, umount]
  );

  return (
    <PortalContext.Provider value={manage}>{children}</PortalContext.Provider>
  );
};

export default PortalProvider;

在页面的外层添加 PortalProvider,在指定挂载的位置放置 PortalSlot,然后就可以在 Modal 之类的组件里面使用 Portal 组件包装,实现在指定的 dom 位置渲染。

<Portal.Provider>
  {children}
  <Portal.Slot></Portal.Slot>
</Portal.Provider>
<Portal>
  <Modal>...</Modal>
</Portal>
8

有个疑惑,Portal.Provider挂到app.js中的render可以的吗

7

Taro 已具备 createPortal 的能力,示例如下:

import { useState, useEffect } from 'react'
import { createPortal } from "@tarojs/react";

export default function Index() {
  const [dom, setDom] = useState(null);

  useEffect(() => {
    const dom = document.getElementById("my-portal");
    setDom(dom);
    setTimeout(() => {
      setDom(null);
    }, 3000);
  }, []);

  return (
    <View className="index">
      <Text>Hello world!</Text>
      <View id="my-portal"></View>
      {dom && createPortal(<View>你好世界</View>, dom)}
    </View>
  );
}

其中, dom 必须是 TaroElement 的实例。

同时,微信小程序也提供了 root-portal 组件,原生支持了 Portal 的能力。在 Taro 中使用如下:

  import { useState } from 'react'
  import { RootPortal, View, Button } from '@tarojs/components'

  export default function RootPortalExample {
    const [show, setShow] = useState(false)
    const toggle = () => {
      setShow(!show)
    }
    render () {
      return (
        <View>
          <Button onClick={toggle}>显示root-portal</Button>
          {
            show && (<RootPortal><View>content</View></RootPortal>)
          }
        </View>
      )
    }
  }
5

依赖createPortal实现root-portal

import { useRouter } from '@tarojs/taro'
import { createPortal } from "@tarojs/react";
import { useLayoutEffect, useState } from "react";

const RootPortal = ({ children, enable = true }) => {
  const router = useRouter()
  const [dom, setDom] = useState()

  useLayoutEffect(() => {
    const _dom = document.getElementById(router.$taroPath);
    _dom && setDom(_dom)
  }, [router.$taroPath])

  return (enable && dom) ? createPortal(children, _dom) : children
}

export default RootPortal
4

请问这种方式小程序支持吗

9

只要是支持Taro的小程序都支持, 他操作的是react的虚拟dom

5

啥也不说了,大哥牛逼