Skip to content

Instantly share code, notes, and snippets.

@icedcrow
Last active August 12, 2024 14:16
Show Gist options
  • Save icedcrow/c0a43e0a2d898333416f2dba47a86bee to your computer and use it in GitHub Desktop.
Save icedcrow/c0a43e0a2d898333416f2dba47a86bee to your computer and use it in GitHub Desktop.
react-native multiple modal
import { createContext, useContext, useState } from 'react';
export function useStackModalContextCreator() {
const [list, setList] = useState([]);
const [stack, setStack] = useState([]);
const [hasRenderEntry, setHasRenderEntry] = useState(false);
return { list, setList, stack, setStack, hasRenderEntry, setHasRenderEntry };
}
export const StackModalContext = createContext(null);
/**
* @returns {ReturnType<typeof useStackModalContextCreator>}
*/
export function useStackModalContext() {
return useContext(StackModalContext);
}
export { StackModal } from './StackModal';
export { StackModalProvider } from './StackModalProvider';
export { StackModalRenderEntry } from './StackModalRenderEntry';
import React, { useEffect, useRef } from 'react';
import { useStackModalContext } from './context';
/**
* @typedef {import('react-native').ModalProps} ModalProps
* @param {ModalProps} props
*/
export function StackModal(props) {
const {visible} = props;
const {setList, setStack} = useStackModalContext();
const componentIdRef = useRef(`${Math.random()}`);
useEffect(() => {
const componentId = componentIdRef.current;
setList((prev) => [...prev, {id: componentId, props}]);
return () => {
setList((prev) => prev.filter(({id}) => id !== componentId));
};
}, [setList, props]);
useEffect(() => {
const componentId = componentIdRef.current;
if (visible) {
setStack((prev) => [...prev, componentId]);
}
return () => {
setStack((prev) => prev.filter((id) => id !== componentId));
};
}, [setStack, visible]);
return null;
}
import React from 'react';
import { StackModalContext, useStackModalContextCreator } from './context';
import {StackModalRenderEntryInternal} from './StackModalRenderEntry.internal';
/**
* @param {import('react').PropsWithChildren} props
*/
export function StackModalProvider(props) {
const context = useStackModalContextCreator();
const {hasRenderEntry} = context;
return (
<StackModalContext.Provider value={context}>
{props.children}
{!hasRenderEntry ? <StackModalRenderEntryInternal /> : null}
</StackModalContext.Provider>
);
}
import React from 'react';
import {useStackModalContext} from './context';
import {Wrapper} from './Wrapper';
export function StackModalRenderEntryInternal() {
const {list, stack} = useStackModalContext();
const firstId = stack[0];
const firstItem = firstId ? list.find(({id}) => id === firstId) : null;
return firstItem ? <Wrapper componentId={firstId} {...firstItem.props} /> : null;
}
import React, {useEffect} from 'react';
import {useStackModalContext} from './context';
import {StackModalRenderEntryInternal} from './StackModalRenderEntry.internal';
/**
* @param {{context?: ReturnType<import('./context').useStackModalContext>}} props
*/
export function StackModalRenderEntry(props) {
const {context: incomeContext} = props;
const defaultContext = useStackModalContext();
const context = incomeContext || defaultContext;
const {setHasRenderEntry} = context;
useEffect(() => {
setHasRenderEntry(true);
return () => {
setHasRenderEntry(false);
};
}, [setHasRenderEntry]);
return <StackModalRenderEntryInternal />;
}
import React from 'react';
import { Modal } from 'react-native';
import { useStackModalContext } from './context';
/**
* @typedef {import('react-native').ModalProps} ModalProps
* @param {ModalProps & {componentId: string}} props
*/
export function Wrapper(props) {
const {children, componentId, ...restProps} = props;
const {list, stack} = useStackModalContext();
const index = stack.findIndex((v) => v === componentId);
const nextId = index >= 0 ? stack[index + 1] : null;
const nextItem = nextId ? list.find(({id}) => id === nextId) : null;
return (
<Modal {...props}>
{children}
{nextItem ? <Wrapper componentId={nextId} {...nextItem.props} /> : null}
</Modal>
);
}
@icedcrow
Copy link
Author

icedcrow commented Jul 24, 2024

May cause the components inside to detach from the external context.

import {createContext, useContext} from 'react';

const SomeContext = createContext(null);

function useSomeContext() {
  return useContext(SomeContext);
}

function Outer() {
  const context = {};
  return;
  <StackModalProvider>
    <SomeContext.Provider value={context}>
      <Content1 />
      <Content2 />
    </SomeContext.Provider>
    {/* <StackModal /> will be moved here, causing it to leave SomeContext. */}
  </StackModalProvider>;
}

function Content1() {
  return (
    <StackModal>
      <Inner1 />
    </StackModal>
  );
}
function Content2() {
  return (
    <StackModal>
      <Inner2 />
    </StackModal>
  );
}

function Inner1() {
  const context = useSomeContext(); // null here
  return <View />;
}
function Inner2() {
  const context = useSomeContext(); // null here
  return <View />;
}

The first solution is to move inside SomeContext.

function Outer() {
  const context = {};
  return (
    <SomeContext.Provider value={context}>
      <StackModalProvider>
        <Content1 />
        <Content2 />
      </StackModalProvider>
    </SomeContext.Provider>
  );
}
/** ... */
function Inner1() {
  const context = useSomeContext(); // ok
  return <View />;
}
function Inner2() {
  const context = useSomeContext(); // ok
  return <View />;
}

Another solution is to re-establish a context within and pass the external context through to the internal context.

function Outer() {
  const context = {};
  return (
    <SomeContext.Provider value={context}>
      <StackModalProvider>
        <Content1 />
        <Content2 />
      </StackModalProvider>
    </SomeContext.Provider>
  );
}

function Content1() {
  const context = useSomeContext();
  return (
    <StackModal>
      <SomeContext.Provider value={context}>
        <Inner1 />
      </SomeContext.Provider>
    </StackModal>
  );
}
function Content2() {
  const context = useSomeContext();
  return (
    <StackModal>
      <SomeContext.Provider value={context}>
        <Inner2 />
      </SomeContext.Provider>
    </StackModal>
  );
}

function Inner1() {
  const context = useSomeContext(); // ok
  return <View />;
}
function Inner2() {
  const context = useSomeContext(); // ok
  return <View />;
}

Or use to specify the location where will be rendered.

function Outer() {
  const context = {};
  return;
  <StackModalProvider>
    <SomeContext.Provider value={context}>
      <Content1 />
      <Content2 />
      <StackModalRenderEntry />
    </SomeContext.Provider>
  </StackModalProvider>;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment