Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
dtdths opened this issue Aug 7, 2020 · 16 comments
Closed

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

dtdths opened this issue Aug 7, 2020 · 16 comments
Labels
enhancement New feature or request

Comments

@dtdths
Copy link

dtdths commented Aug 7, 2020

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

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

这个 API ### 长什么样?

createPortal(<></>, otherNode)

@tourze
Copy link
Contributor

tourze commented Dec 10, 2020

+1。参考 remaxjs/remax#1046

@sengmitnick
Copy link

+1。

@stillyu
Copy link

stillyu commented Aug 4, 2021

+1

@Jkanon
Copy link

Jkanon commented Oct 30, 2021

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

@tourze
Copy link
Contributor

tourze commented Nov 3, 2021

+1

2 similar comments
@sedationh
Copy link

+1

@busy-dog
Copy link

+1

@pandajk
Copy link

pandajk commented Apr 28, 2022

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

@dtdths
Copy link
Author

dtdths commented May 25, 2022

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

@bylevel
Copy link

bylevel commented Sep 27, 2022

我参考 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
Copy link
Contributor

tourze commented Oct 20, 2022

我参考 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
Copy link
Member

AdvancedCat commented Apr 6, 2023

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
Copy link

lupingW commented Aug 14, 2023

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

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

lupingW commented Aug 17, 2023

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

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

啥也不说了,大哥牛逼

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests