Use constant value as typescript key name - reactjs

I am trying to deine and configure all of my screens for react-navigation in a single file a bit like this:
export const SCREEN_1: ScreenConfig = { path: '/screen-1', component: ... };
export const SCREEN_2: ScreenConfig = { path: '/screen-2', component: ... };
This then allows me to render and navigate to the screens like this:
import * as screens from './screens';
screens.map(screen => <Screen name={screen.path} component={screen.component} />);
...
import { SCREEN_1 } from './screens'
navigation.navigate(SCREEN_1.path, props);
I now also wanna add type checking for the props passed to the navigate function. react-navigation allows me to define types like this:
type ScreenParams = {
'/screen-1': Screen1Props,
'/screen-2': Screen2Props,
}
Is there a way to automatically generate the ScreenParams type withouth having to define the path in a second location.
The best I got so far is something like this:
type ScreenParams = {
'/screen-1': React.ComponentProps<SCREEN_1.component>,
'/screen-2': React.ComponentProps<SCREEN_2.component>,
}

Related

How to succinctly export a wrapped and named function without using two names

I have a function which transforms other functions:
//library:
let transform = function(OriginalComponent) {
let WrappedComponent (props) => {
//some transformation
return <OriginalComponent {...props} />
};
//I specifically need the original component to have a NON EMPTY name here
Object.defineProperty(WrappedComponent, "name", { value: OriginalComponent.name });
}
I currently use this in a file like so
export const MyWrappedComponent = transform(function MyComponent(props){
return <h1>Hello {props.name}!</h1>;
});
With this setup I currently need to use different names for the export and the function.
My question is: Can I somehow export this in one line, using just one name instead of two?
I tried:
export function transform(function MyComponent(props){
return <h1>Hello {props.name}!</h1>;
});
But that is not valid, as the export has no name.
I also thought of
export const MyComponent = transform((props) => {
return <h1>Hello {props.name}!</h1>;
});
But then transform() receives an unnamed component (and it cannot know the export name I believe?)
This is regarding the standards of a library, so I want to keep the example as clean as possible. Naming a function and then naming the export could get confusing. If I have to name both, I'd prefer to use the same name, but I don't see how.
If you want to use a named export, and you want to pass the function directly into transform, you can't (reasonably¹) get around repeating the name, like this:
export const MyComponent = transform(function MyComponent(props){
return <h1>Hello {props.name}!</h1>;
});
With this setup I currently need to use different names for the export and the function.
Thankfully, you don't; it's perfectly valid to use the same name there, as above.
For what it's worth, there are a couple of issues with the transform function that I noticed:
You can't directly write to the name property of a function, it's read-only. But you can replace the property (because it's configurable) via Object.defineProperty.
It's not returning the wrapped component.
Here's a version with those fixed:
export let transform = function (OriginalComponent) {
let WrappedComponent = (props) => {
//some transformation
return <OriginalComponent {...props} />;
};
// I specifically need the original component to have a NON EMPTY name here
Object.defineProperty(WrappedComponent, "name", {
value: OriginalComponent.name,
});
return WrappedComponent;
};
In that Object.defineProperty call, I haven't specified any of the flags (writable, configurable, enumerable), so the flags from the previous property will be used. (The property is configurable but not writable or enumerable.)
As an alternative, you could put the unwrapped components in an object:
export const components = {
MyComponent(props) {
return <h1>Hello ${props.name}!</h1>;
},
// ...
};
...and then post-process them:
for (const [name, component] of Object.entries(components)) {
components[name] = transform(component);
}
But it means that your export is the components object, not the individual components, so you'd end up with usage like this:
import { components } from "./somewhere";
const { MyComponent } = components;
// ...
...which is less than ideal. (And sadly, you can't directly destructure imports.)

I am trying to dynamically create a Mui icon using React.createElement. However, React lowercases the element. Is there a way to keep case?

I am getting service.icon from JSON, so it looks like so
[
{
"icon": "AdminPanelSettingsIcon",
}
]
I am using React.createElement() like so,
data.map((service) => {
let icon = React.createElement(
service.icon,
{ key: service.icon },
null);
});
my output is <adminpanelsettingsicon></adminpanelsettingsicon>
is there anyway to keep case, or convert to PascalCase so it renders like so <AdminPanelSettingsIcon></AdminPanelSettingsIcon>
Note: Is there anyway to dynamically display mui icons based on specified JSON aka api call.
The docs specify:
React.createElement(
type,
[props],
[...children]
)
The type argument can be either a tag name string (such as 'div' or
'span'), a React component type (a class or a function), or a React
fragment type.
You're attempting to create an element using the string "AdminPanelSettingsIcon". Instead you should be attempting this by passing in the class:
React.createElement(AdminPanelSettingsIcon)
You can do this by converting your list to use classes as the value:
[
{
// The value is a class now, not a string
"icon": AdminPanelSettingsIcon,
}
]
Update:
If you are using https://www.npmjs.com/package/#material-ui/icons (or any other library), you can dynamically import all of the components like this:
//Preparing, it should run inside async function
let maps = {}
let listIcon = [];
await import('#material-ui/icons').then(res => {
listIcon = res
})
for(let icon of listIcon){
maps[icon.constructor.name.toLowerCase()] = icon;
}
//Render
data.map((service) => {
const component = maps[service.icon] || DefaultComponent;
let icon = React.createElement(
component,
{ key: component },
null);
});
Original answer:
If the API output is predictable (service.icon is just a component of the components you defined)
You can do this:
//Import...
//Defined maps
const maps = {
"adminpanelsettingsicon": AdminPanelSettingsIcon,
"adminpanelsettingsicon2": AdminPanelSettingsIcon2,
"adminpanelsettingsicon3": AdminPanelSettingsIcon3,
}
//Render
data.map((service) => {
const component = maps[service.icon] || DefaultComponent;
let icon = React.createElement(
component,
{ key: component },
null);
});
It'll work as you wish
Simple way to do this,
In your .jsx file :
import Icon from '#mui/material/Icon';
export default function MyComponent() {
const iconName = `home`
return (<Icon>{iconName}</Icon>);
}
As a prerequisite, you must include this line in yout index.html
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
Voilà !
Sources :
Mui DOC

How to create React elements in an array (with props) and then render them by simply mapping over that array?

I am wondering if there is a way to create elements in an array and then simply render them, if that is possible it will simplify my code a bit.
Right now I am defining sections for a page in one file, then that gets sent to another and so on, so at the moment it looks like this
const heroSectionProps = { ... }
const pageLayout = {
sections: [{name: "Hero", props: heroSectionProps}]
}
and later when rendering the page I am doing the following:
import { Hero } from "~/sections"
const section = {
Hero: Hero
}
export const Page = ({ sections, ...props }) => {
return sections.map(({ name, props }) => {
const Section = section[name];
return <Section {...props} />
})
}
And that works just fine, now the question is, is it possible to somehow move to a syntax that would look more like this:
import { Hero } from "~/sections"
const heroSectionProps = { ... }
const pageLayout = {
sections: [Hero(heroSectionProps)]
}
and then in page:
export const Page = ({ sections, ...props }) => {
return sections.map(Section => (<Section />))
}
So the idea is to already have the components created within the layout object and then to simply pass them to the page to directly render them. Of course, doing it the above way, I got an error:
Warning: React.jsx: type is invalid -- expected a
string (for built-in components) or a class/function (for composite components) but got: <HeroContainer />. Did you accidentally export a JSX literal
instead of a component?
And other similar about invalid type.
So is that even possible, worth it and how?
You can render an array, so long as it's filled with renderable values (strings, numbers, elements and components). So, say we have an array of components:
import Hero from '../Hero';
const componentsArray = [<p>Hello world!</p>, <div><img src="images/flower.png" /></div>, <Hero {...heroSectionProps} />];
You can instantly render it:
const Component = (props) => {
//...
return componentsArray;
};

How can I make a native app that will pick and design charts according to the name fetched from API?

I want my TabDashboardDetails.js to find out which chart to be displayed according to the name of the chart fetched from API. In TabDashboardDetails.js I want to replace CogniAreaChart with a component that will have specific view for fetched chart and can also take data from API.
Here is my TabDashboardDetails.js
import React from 'react';
import DefaultScrollView from '../components/default/DefaultScrollView';
import ChartView from '../components/default/ChartView';
import CogniAreaChart from '../components/CogniAreaChart';
import { mapNameToChart } from '../utils/commonFunctions';
import { areaChartData } from '../chartData';
const TabDashboardDetail = ({ navigation, route }) => {
const tabsConfig = route.params.tabsConfig;
const ChartToDispay = mapNameToChart();
return (
<DefaultScrollView>
{tabsConfig.components.map((comp) => {
console.log(tabsConfig.components);
return (
<ChartView key={comp.name} title={comp.name}>
<CogniAreaChart
name={comp.name}
areaChartData={areaChartData}
height={200}
/>
</ChartView>
);
})}
</DefaultScrollView>
);
};
export default TabDashboardDetail;
I want to pick charts from commonfunctions.js that I have used:
/* eslint-disable prettier/prettier */
import {
AreaChart,
BarChart,
LineChart,
PieChart,
SingleCircularProgress,
Histogram,
SimpleTable,
BubbleChart,
CandlestickChart,
SankeyChart,
ScatterPlot,
StackedBarChart,
WaterfallChart,
TreeMap,
MixAndMatch,
SimpleCard,
BlogTable,
LiquidTable,
} from 'react-native-svg-charts';
export const mapNameToChart = (name) => {
const nameToChart = {
AreaChart: AreaChart,
BarGraph: BarChart,
LineChart: LineChart,
PieChart: PieChart,
SingleCircularProgress: SingleCircularProgress,
Histogram: Histogram,
SimpleTable: SimpleTable,
BubbleChart: BubbleChart,
CandlestickChart: CandlestickChart,
SankeyChart: SankeyChart,
ScatterPlot: ScatterPlot,
StackedBarGraph: StackedBarChart,
WaterfallTable: WaterfallChart,
TreeMap: TreeMap,
MixAndMatch: MixAndMatch,
SimpleCard: SimpleCard,
BlogCard: BlogTable,
LiquidGauge: LiquidTable,
};
return nameToChart[name];
};
You first need to import all the chart types in the file containing mapNameToChart and map the name to the Chart type accordingly. Then You can try this
const ChartToDispay = mapNameToChart(name);
<ChartToDisplay {...your_props_here} />
In your mapNameToChart function it looks like AreaChart is an actual component and the rest are just string names of components instead of the components themselves. You want to change it so that all of the entries in the map are the components. You want mapNameToChart(name) to return a callable component. Then you can call that component with your props.
I'm not fully understanding your the API comes into play here, but it seems like we get the props by looking up the name? I don't know the the API data comes from, so I'm expecting the components array to be passed as a prop to the CustomChart.
const CustomChart = ({name, components, ...props}) => {
// get the component function/class from your map
const Component = mapNameToChart(chart);
// find the component configuration from your API
const config = components.find(obj => obj.name === name);
// call with props from the config and passed down props
return (
<Component
{...config}
{...props}
/>
)
}

React conditional tag name not working - all letters downsized

I am trying to conditionally render tag name based on prop value.
Example
const SimpleTagName = `Ball${size === 'large' ? 'Large' : 'Small'}`;
return (<SimpleTagName />
but the problem is that I get rendered 'balllarge' tag with all lower case letters. What I am doing wrong ?
Try with this method:
import React from 'react';
import { PhotoStory, VideoStory } from './stories';
const components = {
photo: PhotoStory,
video: VideoStory
};
function Story(props) {
// Correct! JSX type can be a capitalized variable.
const SpecificStory = components[props.storyType];
return <SpecificStory story={props.story} />;
}
Official doc ref to handle this pattern: https://reactjs.org/docs/jsx-in-depth.html#choosing-the-type-at-runtime
JSX gets converted to a React.createElement() call, so what you're doing effectively turns into:
React.createElement('balllarge')
Which is not what you want. You need to pass it an component instead of a string, but you can still determine that dynamically, like so:
import { BallLarge, BallSmall } from './Balls' // or whatever
const Component = ({ size }) => {
const BallComponent = size === 'large' ? BallLarge : BallSmall
return <BallComponent />
}
(If you have more than two options, you may need a different way to handle the mapping between your props and variable types, but the principle remains the same: assign a component to a variable, and then use when rendering.)

Resources