Skip to content

taro 能支持 react-dom 中的 createPortal 吗,或实现类似api #7282

@dtdths

Description

@dtdths

这个特性解决了什么问题?

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

这个 API ### 长什么样?

createPortal(<></>, otherNode)

Activity

tourze

tourze commented on Dec 10, 2020

@tourze
Contributor

+1。参考 remaxjs/remax#1046

sengmitnick

sengmitnick commented on Jul 29, 2021

@sengmitnick

+1。

stillyu

stillyu commented on Aug 4, 2021

@stillyu

+1

Jkanon

Jkanon commented on Oct 30, 2021

@Jkanon

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

tourze

tourze commented on Nov 3, 2021

@tourze
Contributor

+1

sedationh

sedationh commented on Jan 9, 2022

@sedationh

+1

busy-dog

busy-dog commented on Mar 19, 2022

@busy-dog

+1

pandajk

pandajk commented on Apr 28, 2022

@pandajk

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

dtdths

dtdths commented on May 25, 2022

@dtdths
Author

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

bylevel

bylevel commented on Sep 27, 2022

@bylevel

我参考 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>
tourze

tourze commented on Oct 20, 2022

@tourze
Contributor

我参考 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>

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

AdvancedCat

AdvancedCat commented on Apr 6, 2023

@AdvancedCat
Member

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>
      )
    }
  }
lupingW

lupingW commented on Aug 14, 2023

@lupingW

依赖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
uyoungco

uyoungco commented on Aug 17, 2023

@uyoungco

依赖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

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

lupingW

lupingW commented on Aug 17, 2023

@lupingW

依赖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

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

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

uyoungco

uyoungco commented on Aug 17, 2023

@uyoungco

依赖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

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

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

啥也不说了,大哥牛逼

added a commit that references this issue on Apr 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @bylevel@stillyu@sengmitnick@AdvancedCat@Chen-jj

        Issue actions

          taro 能支持 react-dom 中的 createPortal 吗,或实现类似api · Issue #7282 · NervJS/taro