Dynamic loading icon - reactjs

Hi have a script that import location, which consists of multiple svg file and a index.ts file to import and export them.
import * as Icons from '../../icons'
I then have a functional component that returns the icon based on the icon name, which I am getting error.
interface props extends React.SVGProps<SVGSVGElement> {
icon: string
title?: string | undefined
}
function Icon({ icon, ...props }: props) {
const Icon = Icons[icon] <= error here
return <Icon {...props} />
}
function SidebarContent() {
return (
//some code here
<Icon className="w-5 h-5" aria-hidden="true" icon={route.icon} />
//some code here
)
}
The error I am getting is this:
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'typeof import("d:/Code/JS/meal-order/frontend/src/icons/index")'.
No index signature with a parameter of type 'string' was found on type 'typeof import("d:/Code/JS/meal-order/frontend/src/icons/index")'.ts(7053)
I would like to know how in the Icon component. How can it accept icon name (Icon[icon]) and return the correct icon? or probably there is other way to do this ?
More info:
If I write the code like the following then it works without complaining. However that doesn't fit the purpose because my intention is to dynamically load the icon based on the icon name i've passed.
function Icon({ icon, ...props }: props) {
const Icon = Icons.ButtonsIcon
return <Icon {...props} />
}
Update:
This is how I fix the type error:
import react from "react";
import * as Icons from "../../icons";
type IIcon = React.FunctionComponent<React.SVGProps<SVGSVGElement> & {
title?: string | undefined;
}>
export default function Icon({ icon = 'MenuIcon', ...props }) {
const myObj: { [index: string]: IIcon } = Icons
const NewIcon = myObj[icon]
return <NewIcon {...props} />
}

Updated:
Here's a CodeSandbox of what I believe you're trying to do.
Icon.js
import react from "react";
import * as Icons from "./icons";
export default function Icon({ icon, ...props }) {
const NewIcon = Icons[icon];
return <NewIcon {...props} />;
}
Icons/index.js
import { ReactComponent as ButtonIcon } from "./button.svg";
import { ReactComponent as BellIcon } from "./bell.svg";
export { BellIcon, ButtonIcon };
App.js
import Icon from "./Icon";
export default function App() {
return (
<div className="App">
<Icon icon="BellIcon"></Icon>
<Icon icon="ButtonIcon"></Icon>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
https://codesandbox.io/embed/frosty-stitch-li3wiz?fontsize=14&hidenavigation=1&theme=dark
If you posted a codesandbox I could assist better that way, however, as long you Icons are components themselves, not just SVGs, it should work.

Related

React Context Provider override not working

The Goal
Hello ๐Ÿ‘‹๐Ÿป
I have a React project with NextJS where I want to chose whether using light or dark theme depending on the background / section.
My solution
I have found that the best solution to my problem is using React Context API. To change the theme I just need to override the provider with a new one (as indicated in the documentation).
The problem
I have the start of a solution but when overriding the Provider, I noticed that the change isn't effective for every components even if in the devTools the context in theses components is the correct one. ๐Ÿ˜•
I have tried switching from useContext(context) to <context.Consumer> but there was no improvement.
The code:
/context/Theme
import { createContext, JSXElementConstructor, ReactElement, ReactFragment, ReactNode, ReactPortal } from "react";
import { getTheme, Theme } from "../constants/theme";
// type of getTheme: (theme: 'light' | 'dark') => Theme
const ThemeContext = createContext<Theme>(getTheme('light'))
export default ThemeContext
interface ThemeProviderProps {
theme: 'light' | 'dark'
children: string | number | boolean | ReactElement<any, string | JSXElementConstructor<any>> | ReactFragment | ReactPortal | null | undefined
}
interface ThemeConsumerProps {
children: (value: Theme) => ReactNode
}
// The goal here is to make the theme switching more natural
export const ThemeProvider = (props: ThemeProviderProps): JSX.Element => {
return (
<ThemeContext.Provider value={getTheme(props.theme)}>
{props.children}
</ThemeContext.Provider>
)
}
// Just for convenience
export const ThemeConsumer = (props: ThemeConsumerProps): JSX.Element => {
return <ThemeContext.Consumer>{props.children}</ThemeContext.Consumer>
}
/pages/_app (Starting point of NextJS App, equivalent to /src/App
import '../styles/globals.css'
import type { AppProps } from 'next/app'
import { ThemeProvider } from "../context/Theme";
export default function App({ Component, pageProps }: AppProps) {
return (
<ThemeProvider theme="light">
<Component {...pageProps} />
</ThemeProvider>
)
}
/common/Popup/Popup
import {ReactNode, useContext, useState} from "react";
import styles from "./Popup.module.css"
import Typography, {Typos} from "../Typography";
import Icon from "../Icon";
import {CloseIcon} from "../../icons";
import ThemeContext, {ThemeProvider, ThemeConsumer} from "../../context/Theme";
interface Props {
children: ReactNode
title: string
open: boolean
close: () => void
}
export default function (props: Props): JSX.Element {
return (
<>
// ...
<ThemeProvider theme="dark">
<div className={styles.header}>
<Typography level={Typos.TITLE_2} bold>{ props.title }</Typography> // The useContext works inside of this components
<ThemeConsumer>
{(theme) =>
<Icon icon={CloseIcon} color={theme.texts.secondary} size={1.5}
onClick={props.close} clickable />
}
</ThemeConsumer>
</div>
<div className={styles.body}>
{ props.children } // The useContext or the <context.Consumer> doesn't work for the children
</div>
</ThemeProvider>
// ...
</>
)
}
I've just misplaced my <context.Consumer> outside of the Popup component and I learned that useContext doesn't work when overriding the Provider.

How to restrict props.children in react typescript?

import React from 'react';
import './App.css';
import Message from "./components/Message";
function App() {
return (
<div className="App">
<Message>
<p>Hello World</p>
</Message>
<Message>
<a>Hello World</a>
</Message>
<Message>
<button>Hello World</button>
</Message>
</div>
);
}
export default App;
import React, {FunctionComponent} from 'react';
interface OwnProps {
children: React.ReactElement<HTMLParagraphElement>;
}
type Props = OwnProps;
const Message: FunctionComponent<Props> = ({children}) => {
return (
<h1>
{children}
</h1>
);
};
export default Message;
The above code I can pass any HTMLElementTag as props.
Is there any way to restrict ReactElement to only specific element ?
For example only with p tag or button tag etc.
You can render specific children with React.Children by checking its type.
Below example only renders p tag elements.
import React, { Children } from "react";
interface Props {
children: React.ReactElement<HTMLParagraphElement | HTMLHeadElement>[] | React.ReactElement<HTMLParagraphElement | HTMLHeadElement>;
}
const Component: React.FC<Props> = ({ children }) => {
const elements = Children.map(children, (child) => {
if ((child as React.ReactElement).type === "p") {
return child;
}
return null;
})?.filter(Boolean);
return <div>{elements}</div>;
};
export default Component;
Note that type of custom component is a function, not string.
And if you want to check types of nested html elements, you need to recursively check types of react elements children.
I do not know what you are trying to do here, but there may be a better approach than restricting render of children elements.
I think this is the wrong way to be going about implementing HTMl here: if you want a <h1> element to render then you shouldn't be passing <p> or <button> elements into it.
Why not just leave the functional component input here as just a string? Like so:
import React from 'react';
interface MessageProps {
messageText: string;
}
const Message: React.FC<MessageProps> = ({ messageText }) => {
return (
<h1>
{messageText}
</h1>
);
};
export default Message;
//////
import React from 'react';
import Message from "./components/Message";
function App() {
return (
<div className="App">
<Message messageText="Hello world" />
</div>
);
}
export default App;
you can use following for static checking:
type Props = { children: React.ReactNode } //-- allow any React valid Node
// type Props = { children: never } //-- will not allow children to be passed

Custom icon wrapper component

I want to use react-feather icons but within a standard size button component I created.
To use a react-feather component I need to create it like so <Camera size={18} /> or <Icon.Camera size={18} /> however, I don't want to keep on repeating the size and color and any other prop that stays the same.
How can I make the icon type dynamic within my Button.
I know this form is invalid but I am looking for a way that is and that is the best I could write to present my thought.
import React, { FC } from "react";
import * as Icons from "react-feather";
interface IconProps {
icon?: Icons.Icon;
}
const IconComponent: FC<IconProps> = (props) => {
return (
<button>
{props.icon && <Icons[props.icon] size={18} />}
{props.children}
</button>
);
};
export default IconComponent;
The IconComponent component below should do. You can see it live in this codesandbox.
// Icon.tsx
import React, { ReactNode } from "react";
import { Icon } from "react-feather";
interface IconComponentProps {
icon?: Icon;
children: ReactNode;
}
const IconComponent = ({ icon: FeatherIcon, children }: IconComponentProps) => {
return (
<button>
{FeatherIcon && <FeatherIcon size={18} />}
{children}
</button>
);
};
export default IconComponent;
There you could use it anywhere:
// App.tsx
import IconComponent from "./Icon";
import { Camera, Airplay } from "react-feather";
import "./styles.css";
export default function App() {
return (
<div className="App">
<IconComponent icon={Camera}>
<h1>Camera</h1>
</IconComponent>
<IconComponent icon={Airplay}>
<h1>Airlpay</h1>
</IconComponent>
<IconComponent>
<h1>No Icon</h1>
</IconComponent>
</div>
);
}

Error when calling theme Props in component using ReactJs and Typescript

I need to pass the color of a theme to a component, I'm using type script (I'm a beginner).
I came across an error that I can't solve.
The error is: Parameter 'props' implicitly has an 'any' type.ts(7006)
and it appears in the color line of my code
<Linkxx
color={(props) => props.theme.colors.red}
size={11}
weight={500}
decoration={false}
/>
My component
import React from 'react';
import { Container } from './styles';
interface LinkProps {
color: string;
size?: number;
weight?: number;
decoration?: boolean;
}
const Link: React.FC<LinkProps> = ({ color, size, weight, decoration }) => {
return (
<Container
decoration={decoration}
weight={weight}
size={size}
color={color}
to="/teste"
>
teste
</Container>
);
};
export default Link;
The code where the problem occurs
import React from 'react';
import Header from '../../components/Header';
import Linkxx from '../../components/Link';
import {
Container,
Content,
UserCard,
Avatar,
UserData,
Canais,
BoxAction,
} from './styles';
const User: React.FC = () => {
return (
<>
<Header />
<Container>
<Content>
<h1>User</h1>
<UserCard>
<Avatar />
<UserData>
<span>Sample Name</span>
<small>sample#gmail.com</small>
</UserData>
<Canais>
<span>Adm, Manager</span>
<div>
<small>test</small>
<small>test</small>
<small>test</small>
</div>
</Canais>
<BoxAction>
<div>
<Linkxx
color={(props) => props.theme.colors.red}
size={11}
weight={500}
decoration={false}
/>
</div>
</BoxAction>
</UserCard>
</Content>
</Container>
</>
);
};
export default User;
How can I use the theme properties when calling my component?
Try this:
<Linkxx
// color={(props) => props.theme.colors.red} // You passed a function
color={props.theme.colors.red} // Try passing a value or string i.e. props.theme.colors.red
size={11}
weight={500}
decoration={false}
/>
As your interface says color is string:
interface LinkProps {
color: string; // color is string
size?: number;
weight?: number;
decoration?: boolean;
}
Try adding the type 'any' to your 'props' parameter as such:
color={(props: any) => props.theme.colors.red}
size={11}
weight={500}
decoration={false}
Because in typescript you need to specify the type of props you are going to send or it takes the default type defined. if you don't want to specify any type then explicitly ask the component to expect state and props of โ€˜anyโ€™ type. Refer to this article for detailed solution: React/typescript: Parameter โ€˜propsโ€™ implicitly has an โ€˜anyโ€™ type error

Property value expected type of string but got null

I have an Icon.jsx component:
import React from 'react'; import { css } from 'emotion';
import ArrowRight from "./arrow-right.svg";
export const iconTypes = {
arrowRight: 'ARROW_RIGHT',
//arrowLeft: "ARROW_LEFT", }
const iconSrc = {
ARROW_RIGHT: ArrowRight,
ARROW_LEFT: ArrowLeft, }
export default ({ type }) => {
return (
<Icon>
<img src={iconSrc[type]} />
</Icon>
)
};
And an `Icon.jsx' story:
import React from "react";
import { storiesOf } from "#storybook/react";
import { action } from "#storybook/addon-actions";
import Icon from "../components/Icon/Index";
import { iconTypes } from "../components/Icon/Index";
storiesOf("Icon", module)
.add("with text", () => (
<Icon type={iconTypes.leftArrow}>
</Icon>
));
I keep getting the following error on the Icon.jxs component:
Property value expected type of string but got null
But I can't figure it out, any help would be much appreciated.
The issue was do to the ES6 syntax, that works fine:
export default function Icon({ type }) {
return (
<div>
<img src={iconSrc[type]} />
</div>
)
};
You're using leftArrow in <Icon type={iconTypes.leftArrow}></Icon> which is not defined and thus type={undefined} and passing undefined or null will cause you the error as you've specified.
Property value expected type of string but got null
The fix is to use defined type of arrow:
<Icon type={iconTypes.ARROW_LEFT}></Icon>

Resources