I have a function that takes some arguments and renders an SVG. I want to dynamically import that svg based on the name passed to the function. It looks like this:
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} />;
};
According to the webpack documentation for dynamic imports and the magic comment "eager":
"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."
This is what my Icon is resolved to:
> Module
default: "static/media/antenna.11b95602.svg"
__esModule: true
Symbol(Symbol.toStringTag): "Module"
Trying to render it the way my function is trying to gives me this 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.
I don't understand how to use this imported Module to render it as a component, or is it even possible this way?
You can make use of ref and ReactComponent named export when importing SVG file. Note that it has to be ref in order for it to work.
The following examples make use of React hooks which require version v16.8 and above.
Sample Dynamic SVG Import hook:
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 };
}
Sample Dynamic SVG Import hook in typescript:
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 };
}
For those who are getting undefined for ReactComponent when the SVG is dynamically imported, it is due to a bug where the Webpack plugin that adds the ReactComponent to each SVG that is imported somehow does not trigger on dynamic imports.
Based on this solution, we can temporary resolve it by enforcing the same loader on your dynamic SVG import.
The only difference is that the ReactComponent is now the default output.
ImportedIconRef.current = (await import(`!!#svgr/webpack?-svgo,+titleProp,+ref!./${name}.svg`)).default;
Also note that there’s limitation when using dynamic imports with variable parts. This SO answer explained the issue in detail.
To workaround with this, you can make the dynamic import path to be more explicit.
E.g, Instead of
// App.js
<Icon path="../../icons/icon.svg" />
// Icon.jsx
...
import(path);
...
You can change it to
// App.js
<Icon name="icon" />
// Icon.jsx
...
import(`../../icons/${name}.svg`);
...
Your rendering functions (for class components) and function components should not be async (because they must return DOMNode or null - in your case, they return a Promise). Instead, you could render them in the regular way, after that import the icon and use it in the next render. Try the following:
const Test = () => {
let [icon, setIcon] = useState('');
useEffect(async () => {
let importedIcon = await import('your_path');
setIcon(importedIcon.default);
}, []);
return <img alt='' src={ icon }/>;
};
I made a change based on answer 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(`!!#svgr/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;
};
One solution to load the svg dynamically could be to load it inside an img using require, example:
<img src={require(`../assets/${logoNameVariable}`)?.default} />
i changed my code to this and work:
import { ReactComponent as Dog } from './Dog.svg';
use like this:
<Dog />
or if it is dynamic:
import * as icons from '../../assets/categoryIcons';
const IconComponent = icons[componentName??'Dog'];
<IconComponent fill='red' />
I dynamically fetched the SVG file as text and then put the SVG within a div dangerouslySetInnerHTML.
const Icon = ({ className, name, size = 16 }: IconProps) => {
const [Icon, setIcon] = React.useState("");
React.useEffect(() => {
fetch(`/icons/${name}.svg`)
.then((res) => res.text())
.then((res) => {
if (res.startsWith("<svg")) return setIcon(res);
console.error(
`Icon: "${name}.svg" not found in ${process.env.PUBLIC_URL}/icons`
);
return setIcon("");
});
}, [name]);
if (!Icon) return null;
return (
<div
className={classNames("icon", className)}
style={{ width: !size ? "100%" : size + "px", height: "100%" }}
dangerouslySetInnerHTML={{ __html: Icon }}
/>
);
};
Preview on Codesandbox
You can automatically change the color of your svg by giving it a fill value of "currentColor".
Related
I want to render overlay on the long running operations.
Consider I have the following code
let spinnerState = useRecoilValue(overlayState);
return <BrowserRouter>
<Spin indicator={<LoadingOutlined />} spinning={spinnerState.shown} tip={spinnerState.content}>.........</BrowserRouter>
What I do in different components
const [, setOverlayState] = useRecoilState(overlayState);
const onButtonWithLongRunningOpClick = async () => {
Modal.destroyAll();
setOverlayState({
shown: true,
content: text
});
try {
await myApi.post({something});
} finally {
setOverlayState(overlayStateDefault);
}
}
How can I refactor this to use such construction that I have in this onbuttonclick callback? I tried to move it to the separate function, but you cannot use hooks outside of react component. It's frustrating for me to write these try ... finally every time. What I basically want is something like
await withOverlay(async () => await myApi.post({something}), 'Text to show during overlay');
Solution
Write a custom hook that includes both UI and API. This pattern is widely used in a large app but I couldn't find the name yet.
// lib/buttonUi.js
const useOverlay = () => {
const [loading, setLoading] = useState(false);
return {loading, setLoading, spinnerShow: loading };
}
export const useButton = () => {
const overlay = useOverlay();
const someOp = async () => {
overlay.setLoading(true);
await doSomeOp();
/* ... */
overlay.setLoading(false);
}
return {someOp, ...overlay}
}
// components/ButtonComponent.jsx
import { useButton } from 'lib/buttonUi';
const ButtonComponent = () => {
const {spinnerShow, someOp} = useButton();
return <button onClick={someOp}>
<Spinner show={spinnerShow} />
</button>
}
export default ButtonComponent;
create a custom hook that handles the logic for showing and hiding the overlay.
import { useRecoilState } from 'recoil';
const useOverlay = () => {
const [, setOverlayState] = useRecoilState(overlayState);
const withOverlay = async (fn: () => Promise<void>, content: string) => {
setOverlayState({ shown: true, content });
try {
await fn();
} finally {
setOverlayState(overlayStateDefault);
}
};
return withOverlay;
};
You can then use the useOverlay hook in your components
import { useOverlay } from './useOverlay';
const Component = () => {
const withOverlay = useOverlay();
const onButtonWithLongRunningOpClick = async () => {
await withOverlay(async () => await myApi.post({ something }), 'Text to show during overlay');
};
return <button onClick={onButtonWithLongRunningOpClick}>Click me</button>;
};
I have a component where-in I need to fetch some data and render it. The component gets rendered initially. The problem I'm facing is when the handler function switchDocumentType is called after clicking the button for a particular type, the whole component gets unmounted/un-rendered.
While debugging on my own I found this happens after setDocumentType is run inside event handler function.
What is wrong in the below code snippet that could possibly cause this issue? I can see the useEffect is not going in infinite-loop as well.
Code snippet:
import * as React from 'react';
const MyComponent = (props) => {
const [documentType, setDocumentType] = React.useState('alpha');
const [documentData, setDocumentData] = React.useState('');
const types = ['alpha', 'beta', 'gamma'];
React.useEffect(() => {
myDataFetch('https://example.com/foo/?bar=123').then(async (response) => {
const data = await response.json();
setDocumentData(data.terms); // html string
const myDiv = document.getElementById('spacial-div');
myDiv.innerHTML = data; // need to render raw HTML inside a div
});
}, [documentType]);
const switchDocumentType = (type) => {
setDocumentType(type);
// send some analytics events
};
const convertToPDF = () => {
// uses documentData to generate PDF
};
return (
<div className="container-div">
{types.map((type) => {
return (
<button key={type} onClick={(type) => switchDocumentType(type)}>
{type}
</button>
);
})}
<div id="special-div" />
</div>
);
};
export default MyComponent;
You shouldn't edit the DOM directly. React has two DOMs, a virtual DOM and a real DOM. Rendering can be a bit finicky if you decide to edit the real DOM.
You can parse html safely, by using html-react-parser. This is the best way to do it, because it becomes part of the react tree whereas dangerouslySetInnerHTML will replace the entire HTML to flush changes to the DOM. With reconciliation, it can create exponential load times.
It will also sanitize your inputs, you know.. for safety. :)
import parse from 'html-react-parser';
const SpecialDiv = ({html}) => {
const reactElement = parse(html);
return reactElement
}
If you decide that you must use dangerouslySetInnerHTML you can do it as so:
const [someHTML, setSomeHTML] = useState(null)
const someFunction = async() => {
const response = await getData();
const data = await response.json();
setSomeHTML(data);
}
return(
<div>
{someHTML && <div dangerouslySetInnerHTML={{__html: someHTML}} id="special-div"/>}
</div>
)
That being said, I would say that by allowing this, you open yourself up to the possibility of a XSS attack, without properly parsing and purifying your inputs.
Do not use useEffect as handler, use useEffect hooks for initializations.
Instead of using/setting innerHtml, let react do it for you.
I suppose you have myDataFetch defined somewhere and I don't see your data fetch using the type.
Anyways, try to use the modified code below.
import * as React from 'react';
const MyComponent = (props) => {
const [documentType, setDocumentType] = React.useState('alpha');
const [documentData, setDocumentData] = React.useState('');
const types = ['alpha', 'beta', 'gamma'];
const fetchData = async () => {
const response = await myDataFetch('https://example.com/foo/?bar=123')
const data = await response.json();
setDocumentData(data);
}
React.useEffect(() => {
fetchData();
}, []);
const switchDocumentType = async (e, type) => {
e.preventDefault();
setDocumentType(type);
await fetchData();
// send some analytics events
};
return (
<div className="container-div">
{types.map((type) => {
return (
<button key={type} onClick={(e) => switchDocumentType(e, type)}>
{type}
</button>
);
})}
<div id="special-div">{documentData}</div>
</div>
);
};
export default MyComponent;
Not sure why but placing debuggers before state update causes this issue, not only for this component, but for all the other components I tried with. Seems to be an issue either with debugger or React. Removing debuggers solved the issue.
Also, now I'm returning a cleanup function inside useEffect as pointed out in some stack-overflow posts. I also refactored the code as suggested by #iaq and #sheepiiHD to follow React best practices.
Updated code:
import * as React from 'react';
const MyComponent = (props) => {
const [documentType, setDocumentType] = React.useState('alpha');
const [documentData, setDocumentData] = React.useState('');
const types = ['alpha', 'beta', 'gamma'];
const fetchData = async () => {
const response = await myDataFetch('https://example.com/foo/?bar=123')
const data = await response.json();
setDocumentData(data);
}
React.useEffect(() => {
fetchData();
return () => {
setDocumentType('');
setDocumentData('');
};
}, []);
const switchDocumentType = async (e, type) => {
e.preventDefault();
setDocumentType(type);
await fetchData();
// send some analytics events
};
return (
<div className="container-div">
{types.map((type) => {
return (
<button key={type} onClick={(e) => switchDocumentType(e, type)}>
{type}
</button>
);
})}
<div id="special-div" dangerouslySetInnerHTML={{__html: documentData.terms}} />
</div>
);
};
export default MyComponent;
I'm trying to refresh the results on the page, but the refresh button is in a child component to where my React Hook is originally called.
export const ParentComponent = ({
}) => {
const infoINeed = useSelector(getInfoINeed);
const { error, isLoading, data } = useMyAwesomeHook(infoINeed.name);
return (
<div>
<Header/>
<Body className={classes.body}>
<div>Hello Stack overflow</div>
<Body>
</div>
);
};
My Awesome hook looks like this
export const useDogCounts = (name: string | undefined) => {
const { data: token, error: authError } = useAuthHook();
const [error, setError] = useState<Error | null>(null);
const [isLoading, setLoading] = useState(true);
useEffect(() => {
const fetchMyData = async () => {
const request = myRequest(name);
try {
setLoading(true);
const counts = callMyFunction()
setMyData(counts);
setLoading(false);
} catch (requestError) {
if (requestError === null) {
setError(requestError);
} else {
throw requestError;
}
setLoading(false);
}
};
fetchMyData();
}, [name, token]);
return {
data: dogCounts,
error,
isLoading,
};
};
then in my <Header/> component, I have a refresh button that I want to call the hook.
import React, { FC } from 'react';
import { Button } from '#material-ui/core';
export const Header: FC<HeaderProps> = ({}) => {
return (
<Page.Header className={classes.headerWrapper}>
<Page.Title>Dog Counts</Page.Title>
<Button
onClick={() => {}} // functionality to go here
>
Refresh
</Button>
</Header>
);
};
I've tried a couple approaches, including passing a variable into the useDogCount hook called refresh, which the Header component changes in the state in order to trigger the useEffect hook in my main hook. It seemed a bit messy to do it this way and introduce a new variable to keep track of.
I also have implemented something like this elsewhere a different time where I did not use a useEffect hook inside my custom hook, and instead passed the Promise back to the required place to refresh it. However, I need the useEffect hook here to check for updating name or token.
You can return the function used to fetch the data from your custom hook :
export const ParentComponent = () => {
const infoINeed = useSelector(getInfoINeed);
const { error, isLoading, data, fetchData } = useMyAwesomeHook(infoINeed.name);
return (
<div>
<Header onClickRefresh={fetchData}/>
<Body className={classes.body}>
<div>Hello Stack overflow</div>
<Body>
</div>
);
};
export const useDogCounts = (name: string | undefined) => {
const { data: token, error: authError } = useAuthHook();
const [error, setError] = useState<Error | null>(null);
const [isLoading, setLoading] = useState(true);
const fetchData = useCallback(async () => {
... // code to fetch the data
}, [name, token]);
useEffect(fetchData, [fetchData]);
return {
data: dogCounts,
fetchData,
error,
isLoading,
};
};
export const Header: FC<HeaderProps> = ({onClickRefresh}) => {
return (
<Page.Header className={classes.headerWrapper}>
<Page.Title>Dog Counts</Page.Title>
<Button onClick={onClickRefresh}>
Refresh
</Button>
</Header>
);
};
Right now there is no connection between your hook and either of components in terms of firing the request for the data. What I would suggest is to add a function to your hook that is going to call your api and return that function from the hook
export const useDogCounts = (name: string | undefined) => {
const { data: token, error: authError } = useAuthHook();
const [error, setError] = useState<Error | null>(null);
const [isLoading, setLoading] = useState(true);
const callAnApi = async () => {
// ... body of the useEffect
}
useEffect(() => {
const fetchMyData = async () => {
const request = myRequest(name);
try {
setLoading(true);
const counts = callMyFunction()
setMyData(counts);
setLoading(false);
} catch (requestError) {
if (requestError === null) {
setError(requestError);
} else {
throw requestError;
}
setLoading(false);
}
};
fetchMyData();
}, [name, token]);
return {
data: dogCounts,
error,
isLoading,
};
};
then in your ParentComponent you can destructure it as
const { error, isLoading, data, callAnApi } = useMyAwesomeHook(infoINeed.name);
and pass it to Header component as prop where you just use it as
<Button
onClick={callAnApiHandler}
>
Refresh
</Button>
Then you could call this new function inside your useEffect for further refactor
My goal is to use custom hooks created from Context to pass and modify stored values
The final goal is to use something like useFeedContext() to get or modify the context values
What I am actually getting is either the functions that I call are undefined or some other problem ( I tried multiple approaches)
I tried following this video basics of react context in conjunction with this thread How to change Context value while using React Hook of useContext but I am clearly getting something wrong.
Here is what I tried :
return part of App.js
<FeedProvider mf={/* what do i put here */}>
<Navigation>
<HomeScreen />
<ParsedFeed />
<FavScreen />
</Navigation>
</FeedProvider>
Main provider logic
import React, { useState, useEffect, useContext, useCallback } from "react";
import AsyncStorage from "#react-native-async-storage/async-storage";
const FeedContext = React.createContext();
const defaultFeed = [];
const getData = async (keyName) => {
try {
const jsonValue = await AsyncStorage.getItem(keyName);
return jsonValue != null ? JSON.parse(jsonValue) : null;
} catch (e) {
console.log(e);
}
};
const storeData = async (value, keyName) => {
console.log(value, keyName);
try {
const jsonValue = JSON.stringify(value);
await AsyncStorage.setItem(keyName, jsonValue);
} catch (e) {
console.log(e);
}
};
export const FeedProvider = ({ children, mf }) => {
const [mainFeed, setMainFeed] = useState(mf || defaultFeed);
const [feedLoaded, setFeedLoaded] = useState(false);
let load = async () => {
let temp = await AsyncStorage.getItem("mainUserFeed");
temp != null
? getData("mainUserFeed").then((loadedFeed) => setMainFeed(loadedFeed))
: setMainFeed(defaultFeed);
setFeedLoaded(true);
};
useEffect(() => {
load();
}, []);
useCallback(async () => {
if (!feedLoaded) {
return await load();
}
}, [mainFeed]);
const setFeed = (obj) => {
setMainFeed(obj);
storeData(mainFeed, "mainUserFeed");
};
return (
<FeedContext.Provider value={{ getFeed: mainFeed, setFeed }}>
{children}
</FeedContext.Provider>
);
};
//export const FeedConsumer = FeedContext.Consumer;
export default FeedContext;
The custom hook
import { useContext } from "react";
import FeedContext from "./feedProviderContext";
export default function useFeedContext() {
const context = useContext(FeedContext);
return context;
}
What I would hope for is the ability to call the useFeedContext hook anywhere in the app after import like:
let myhook = useFeedContext()
console.log(myhook.getFeed) /// returns the context of the mainFeed from the provider
myhook.setFeed([{test:1},{test:2}]) /// would update the mainFeed from the provider so that mainFeed is set to the passed array with two objects.
I hope this all makes sense, I have spend way longer that I am comfortable to admit so any help is much appreciated.
If you want to keep using your useFeedContext function, I suggest to move it into the your 'Provider Logic' or I'd call it as 'FeedContext.tsx'
FeedContext.tsx
const FeedContext = createContext({});
export const useFeedContext = () => {
return useContext(FeedContext);
}
export const AuthProvider = ({children}) => {
const [mainFeed, setMainFeed] = useState(mf || defaultFeed);
...
return (
<FeedContext.Provider value={{mainFeed, setMainFeed}}>
{children}
</FeedContext.Provider>
);
};
YourScreen.tsx
const YourScreen = () => {
const {mainFeed, setMainFeed} = useFeedContext();
useEffect(() => {
// You have to wait until mainFeed is defined, because it's asynchronous.
if (!mainFeed || !mainFeed.length) {
return;
}
// Do something here
...
}, [mainFeed]);
...
return (
...
);
};
export default YourScreen;
I am trying to do a withCache HoC component but having some problems...
Thats the HoC:
// HOC for cached images
const withCache = (Component) => {
const Wrapped = (props) => {
console.log(props);
const [uri, setUri] = useState(null);
useEffect(() => {
(async () => {
const { uri } = props;
const name = shorthash.unique(uri);
const path = `${FileSystem.cacheDirectory}${name}`;
const image = await FileSystem.getInfoAsync(path);
if (image.exists) {
console.log("Read image from cache");
setUri(image.uri);
return;
} else {
console.log("Downloading image to cache");
const newImage = await FileSystem.downloadAsync(uri, path);
setUri(newImage.uri);
}
})();
}, []);
return <Component {...props} uri={uri} />;
};
Wrapped.propTypes = Component.propTypes;
return Wrapped;
};
export default withCache;
The thing is that "Component" is a custom Image component with specific propTypes and defaultProps.
How do I use this component? I have tried:
const CachedImage = withCache(<MyCustomImage uri={"https://..."} height={100} ripple />)
...
return (<CachedImage />)
but not working :( What I want is to pass a boolean prop to my custom image component named "cached", and if true return the custom image component wrapped in the HOC
In order to use the HOC, you would create the instance outside of the functional component like
const CachedImage = withCache(MyCustomImage)
and use it like
const MyComp = () => {
...
return (<CachedImage uri={"https://..."} height={100} ripple />)
}
Final implementation of the HoC, in case someone finds it useful in the future.
import React, { useState, useEffect } from "react";
import shorthash from "shorthash";
import * as FileSystem from "expo-file-system";
// HOC for cached images
const withCache = (Component) => {
const Wrapped = (props) => {
console.log(props);
const [uri, setUri] = useState(null);
useEffect(() => {
(async () => {
const { uri } = props;
const name = shorthash.unique(uri);
const path = `${FileSystem.cacheDirectory}${name}`;
const image = await FileSystem.getInfoAsync(path);
if (image.exists) {
console.log("Read image from cache");
setUri(image.uri);
return;
} else {
console.log("Downloading image to cache");
const newImage = await FileSystem.downloadAsync(uri, path);
setUri(newImage.uri);
}
})();
}, []);
// Needs to have the final uri before render the image
return uri && <Component {...props} uri={uri} />;
};
Wrapped.propTypes = Component.propTypes;
return Wrapped;
};
export default withCache;