Cómo importar SVG dinámicamente y renderizarlo en línea

14

Tengo una función que toma algunos argumentos y genera un SVG. Quiero importar dinámicamente ese svg según el nombre pasado a la función. Se parece a esto:

import React from 'react';

export default async ({name, size = 16, color = '#000'}) => {
  const Icon = await import(/* webpackMode: "eager" */ `./icons/${name}.svg`);
  return <Icon width={size} height={size} fill={color} />;
};

De acuerdo con la documentación del paquete web para importaciones dinámicas y el comentario mágico "ansioso":

"Generates no extra chunk. All modules are included in the current chunk and no additional network requests are made. A Promise is still returned but is already resolved. In contrast to a static import, the module isn't executed until the call to import() is made."

Esto es lo que mi icono está resuelto a:

> Module
default: "static/media/antenna.11b95602.svg"
__esModule: true
Symbol(Symbol.toStringTag): "Module"

Intentar representarlo de la forma en que mi función está tratando de darme este error:

Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead.

No entiendo cómo usar este Módulo importado para renderizarlo como un componente, ¿o es posible de esta manera?

6
  • ¿Su svg se muestra correctamente si importa estáticamente?
    wederer
    21 abr.20 a las 8:42
  • ¡Sí! Si hago un archivo regular import MyIcon from './icons/myicon.svg', puedo renderizarlo como <MyIcon />.
    Majoren
    21 abr.20 a las 8:44
  • En su lugar, es posible que deba almacenar el SVG resuelto en un estado. 21 abr.20 a las 9:08
  • @dev_junwen Correcto, pero almacenarlo en estado todavía no me permite renderizarlo como un svg en línea.
    Majoren
    21/04/20 a las 13:42
  • O otra forma bastante "dinámica" es tal vez puede definir un mapa de nombre para los componentes SVG, luego usar la sintaxis de notación de corchetes iconMap[name]para recuperar el SVG correcto. No lo he probado todavía, pero creo que podría funcionar. Deberá importar todos los SVG en ese caso y asignarlos al mapa. 22 abr.20 a las 3:49
39

Puede hacer uso de refy ReactComponentel nombre de exportación al importar el archivo SVG. Tenga en cuenta que tiene que ser refpara que funcione.

Los siguientes ejemplos hacen uso de enlaces de React que requieren una versión v16.8y superior.

Ejemplo de gancho de importación dinámica de SVG:

function useDynamicSVGImport(name, options = {}) {
  const ImportedIconRef = useRef();
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState();

  const { onCompleted, onError } = options;
  useEffect(() => {
    setLoading(true);
    const importIcon = async () => {
      try {
        ImportedIconRef.current = (
          await import(`./${name}.svg`)
        ).ReactComponent;
        if (onCompleted) {
          onCompleted(name, ImportedIconRef.current);
        }
      } catch (err) {
        if (onError) {
          onError(err);
        }
        setError(err);
      } finally {
        setLoading(false);
      }
    };
    importIcon();
  }, [name, onCompleted, onError]);

  return { error, loading, SvgIcon: ImportedIconRef.current };
}

Editar react-dynamic-svg-import

Ejemplo de gancho de importación dinámica de SVG en texto mecanografiado:

interface UseDynamicSVGImportOptions {
  onCompleted?: (
    name: string,
    SvgIcon: React.FC<React.SVGProps<SVGSVGElement>> | undefined
  ) => void;
  onError?: (err: Error) => void;
}

function useDynamicSVGImport(
  name: string,
  options: UseDynamicSVGImportOptions = {}
) {
  const ImportedIconRef = useRef<React.FC<React.SVGProps<SVGSVGElement>>>();
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<Error>();

  const { onCompleted, onError } = options;
  useEffect(() => {
    setLoading(true);
    const importIcon = async (): Promise<void> => {
      try {
        ImportedIconRef.current = (
          await import(`./${name}.svg`)
        ).ReactComponent;
        onCompleted?.(name, ImportedIconRef.current);
      } catch (err) {
        onError?.(err);
        setError(err);
      } finally {
        setLoading(false);
      }
    };
    importIcon();
  }, [name, onCompleted, onError]);

  return { error, loading, SvgIcon: ImportedIconRef.current };
}

Editar react-dynamic-svg-import-ts


Para aquellos que están recibiendo undefinedpara ReactComponentcuando el SVG ha sido importada de forma dinámica, que se debe a un error en la webpack plugin que añade el ReactComponentuno al SVG que se importa de alguna manera no lo hace gatillo de importaciones dinámicas.

Basándonos en esta solución , podemos resolverlo temporalmente aplicando el mismo cargador en su importación dinámica de SVG.

La única diferencia es que ReactComponentahora es la defaultsalida.

ImportedIconRef.current = (await import(`[email protected]/webpack?-svgo,+titleProp,+ref!./${name}.svg`)).default;

También tenga en cuenta que existe una limitación al usar importaciones dinámicas con partes variables. Esta respuesta SO explicó el problema en detalle.

Para solucionar este problema, puede hacer que la ruta de importación dinámica sea más explícita.

Por ejemplo, en lugar de

// App.js
<Icon path="../../icons/icon.svg" />

// Icon.jsx
...
import(path);
...

Puedes cambiarlo a

// App.js
<Icon name="icon" />

// Icon.jsx
...
import(`../../icons/${name}.svg`);
...
31
  • 1
    ¡Guau! ¡Gracias, muy limpio! Me había perdido por completo el ReactComponent en cualquier documentación. Creo que aparecerá como un método público en el objeto importado cuando se inspeccione, en mi humilde opinión, pero supongo que no es así como funcionan las cosas aquí. ¡Agradezco la ayuda!
    Majoren
    30 abr.20 a las 9:49
  • 3
    Por cierto, @dev_junwen ReactComponent nunca funcionó para mí, no tengo idea de cómo funciona este código en su CodeSandbox, pero .ReactComponentsolo devuelve indefinido para mí. Tuve que cambiarlo y usarlo .defaultcomo en el ejemplo de @ Enchew a continuación. Encontré una gran documentación sobre esto aquí si baja a "Importación de valores predeterminados": developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
    Majoren
    14 de mayo de 2020 a las 10:41
  • 1
    @Willege He actualizado la respuesta. No dude en echar un vistazo a la muestra de código. 6 jul.20 a las 17:04
  • 2
    @dev_junwen Desafortunadamente, esta solución no me funciona. Lo he intentado todo. Incluso si descargo el código de CodeSandbox e instalo todas las dependencias desde cero npm install, las imágenes SVG no se cargan. Tampoco recibo un mensaje de error. ¿Hay algo que estoy haciendo mal (tal vez configuración webpack, babel o mecanografiado)?
    miu
    9 de julio de 2020 a las 8:49
  • 2
    @mamiu ¿Estás usando la aplicación Create React? De lo contrario, deberá configurar el cargador svg en línea manualmente. Esta respuesta podría ayudar. 9 de julio de 2020 a las 8:56
8

Sus funciones de representación (para componentes de clase) y componentes de función no deben ser asíncronos (porque deben devolver DOMNode o null; en su caso, devuelven una Promesa). En su lugar, puede renderizarlos de la forma habitual, después de eso, importe el icono y utilícelo en el siguiente renderizado. Intente lo siguiente:

const Test = () => {
  let [icon, setIcon] = useState('');

  useEffect(async () => {
    let importedIcon = await import('your_path');
    setIcon(importedIcon.default);
  }, []);

  return <img alt='' src={ icon }/>;
};
5
  • 1
    Esto probablemente funcione para una etiqueta img donde se importará la fuenteIcon.default, como escribió, pero no funciona en mi caso con un svg en línea, donde quiero representarlo como <Icon />. Probé su enfoque con un useEffect asíncrono, pero luego lo necesito setIconcon todo el Módulo, no importadoIcon.default (la ruta del archivo), luego lo renderizo como <Icon />, y me da el errorElement type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
    Majoren
    21 abr.20 a las 12:30
  • ¿Por qué necesita renderizarlo como un <Icon>? ¿Qué le brinda este enfoque? ¿Hay alguna diferencia que no se pueda superar usando<img>
    Enchew
    21/04/20 a las 12:48
  • 1
    ¿Cómo cambiaría las propiedades del svg si es una etiqueta img, como el color de relleno del icono? ¿Es posible con una etiqueta img?
    Majoren
    21/04/20 a las 13:38
  • Los efectos no pueden albergar una asyncfunción. Devuelven una promesa que se invoca como función de limpieza. robinwieruch.de/react-hooks-fetch-data 31/08/20 a las 10:14
  • Independientemente de cuál sea la pregunta original, esta probablemente sea la mejor respuesta para "importar archivos locales dinámicamente". Esto funciona bien cuando tengo una lista de URL de archivos y la carga de forma dinámica.
    Hoon
    5 de ene a las 22:45
2

Hice un cambio basado en la respuesta https://github.com/facebook/create-react-app/issues/5276#issuecomment-665628393

export const Icon: FC<IconProps> = ({ name, ...rest }): JSX.Element | null => {
      const ImportedIconRef = useRef<FC<SVGProps<SVGSVGElement>> | any>();
      const [loading, setLoading] = React.useState(false);
      useEffect((): void => {
        setLoading(true);
        const importIcon = async (): Promise<void> => {
          try {
            // Changing this line works fine to me
            ImportedIconRef.current = (await import(`[email protected]/webpack?-svgo,+titleProp,+ref!./${name}.svg`)).default;
          } catch (err) {
            throw err;
          } finally {
            setLoading(false);
          }
        };
        importIcon();
      }, [name]);

      if (!loading && ImportedIconRef.current) {
        const { current: ImportedIcon } = ImportedIconRef;
        return <ImportedIcon {...rest} />;
      }
      return null;
    };
1

Una solución para cargar el svg dinámicamente podría ser cargarlo dentro de un img usando require , ejemplo:

<img src={require(`../assets/${logoNameVariable}`)?.default} />