Dynamic import and component rendering by string name - reactjs

I have a data set for some simple actions. Each action has a corresponding color and Material-UI icon. There are about 1,100 available icons. The data looks like this:
{ "items":
[{ "action": "Run",
"icon": "DirectionsRunIcon",
"color":"Red"},
{ "action": "Jump",
"icon": "ArrowUpwardIcon",
"color":"Blue"},
{ "action": "Walk",
"icon": "DirectionsWalkIcon",
"color":"Green"},
]}
Each of the icons requires importing a separate library like so:
import ArrowUpwardIcon from '#material-ui/icons/ArrowUpward'
I could be using any number of icons from #material-ui/icons/ and they're obviously stored as a string in the data. So the question is, how can I conditionally render the icons and only load what I need?
Currently I have a function that just does a huge switch, which won't work long term.
function getIcon(action) {
switch (action) {
case "Run":
return <DirectionsRunIcon />;
case "Jump":
return <ArrowUpwardIcon />;
...
Thanks!

Using something like #loadable/component, you could try to load dynamicly the icon with
import loadable from '#loadable/component';
const Icon = ({name, ...rest}) => {
const ImportedIcon = loadable(() =>
import(`#material-ui/icons/${name}`),
);
return <ImportedIcon {...rest} />;
};

Since you have already have the reference name of the Icon in your datset, you may use the function below:
import { find } from 'lodash';
const getIcon = (action) => {
const requiredObj = find(items, { action }) || {};
return requiredObj.icon;
};
Now, you can use the resulting name to do a dynamic import.

Related

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

SurveyJS and NextJS - Issue when setting new state of variable

Version of NextJS: 12.0.8
Version of SurveyJS: 1.9.5
Testing Browser: Chrome, FF
Testing the app (vercel):
You can test the app here: https://jem-surveyjs-nextjs.vercel.app/
Description of the issue:
I combine SurveyJS with NextJS to generate a multi-page survey. The code for the survey is added into a component.
A custom variable can be manipulated using useState.
Every time the variable is set to a different value the survey reloads and jumps back to the first page instead of staying on the current page.
You can clearly see this effect in the dev-tools console.
To replicate the issue simply do the following:
Create a Nextjs app
Add the survey-react module (npm i survey-react)
add the following content to assets/json/survey-json.js
export const surveyJSON = {
"pages": [
{
"name": "page1",
"elements": [
{
"type": "radiogroup",
"name": "Speech Test",
"hasComment": true,
"commentText": "add comment",
"choices": [
"item1",
"item2",
"item3"
]
}
]
},
{
"name": "page2",
"elements": [
{
"type": "radiogroup",
"name": "Try again",
"hasComment": true,
"commentText": "add comment",
"choices": [
"item4",
"item5",
"item6"
]
}
]
}
]
};
Add the following content to components/Survey.js
import {useState, useEffect} from 'react'
import * as SurveyJs from "survey-react";
import "survey-react/survey.css";
import {surveyJSON} from "../assets/js/survey-json";
// Survey Component
const Survey = (props) => {
const [custParam, setCustParam] = useState(false);
// Avoid rehydration conflict
// https://nextjs.org/docs/messages/react-hydration-error
const [hasMounted, setHasMounted] = useState(false);
useEffect(() => {
setHasMounted(true);
}, []);
if (!hasMounted) {
return null;
}
/**
* Handle Custom Function
*/
const handleClick = () => {
// Example 3:
setCustParam(!custParam);
console.log("HANDLE CLICK - CUSTOM PARAM: ", custParam)
};
/**
* afterRenderQuestionHandler
*/
const afterRenderQuestionHandler = function (survey, options) {
// Example 2:
//setCustParam(!custParam);
console.log("AFTER RENDER QUESTION - CUSTOM PARAM: " , custParam)
const tarea = document.querySelector('.sv_q_other');
if ( tarea ) {
tarea.onclick = function(){
handleClick();
}
}
}
let mySurvey = new SurveyJs.Model(surveyJSON);
mySurvey.showCompletedPage = true;
// Example 1:
//setCustParam(true);
console.log("BEFORE RENDER SURVEY - CUSTOM PARAM: " , custParam)
return (
<SurveyJs.Survey
model={mySurvey}
onAfterRenderQuestion={afterRenderQuestionHandler}
/>
);
}
export default Survey;
Finally add this to your pages/index.js
import styles from '../styles/Home.module.css'
import Survey from '../components/Survey'
export default function Home() {
return (
<div>
<Survey />
</div>
)
}
As you can see I have added 3 different examples of how the issue can be created in the components/Survey.js. Example 3 is the active one right now, but you can un-comment Example 2 or Example 1 to see a different effect of the issue.
How can I avoid this reloading and jump to the first page?
Any help that can point me in the right direction would be appreciated
UPDATE:
Many thanks #juliomalves!!
I have added a useState for my Survey Object
const [mySurvey, setMySurvey] = useState({});
and inside the already existing useEffect I've added the following:
setMySurvey( new SurveyJs.Model(surveyJSON) );
I finally just had to remove this line to make it all work:
let mySurvey = new SurveyJs.Model(surveyJSON);

Linking or Routing dynamicaly with Next.js

I am trying to find the best way to link to a details page from a list of objects that are mapped. The list fetches items from an API and works fine. The problem is that I cant pass the id to my details page and get this error when I click on the objects.
:3000/_next/static/development/pages/_app.js?ts=1592696161086:1299 GET http://localhost:3000/flowers/[object%20Object] 404 (Not Found)
and in the url http://localhost:3000/flowers/[object%20Object]
This is what I have in my /pages/flowers.js
import React, { useState, useEffect } from 'react'
import Head from 'next/head'
import Layout from '../components/layout'
import Link from 'next/link'
import utilStyles from '../styles/utils.module.css'
export default function Flowers() {
const LISTURL = 'https://flowers-mock-data.firebaseio.com/flowers.json'
const TESTURL = 'https://flowers-mock-data.firebaseio.com/flowers/9.json'
const [items, setItems] = useState([])
useEffect(() => {
fetch(LISTURL)
.then((res) => res.json())
.then((json) => setItems(json))
}, [] )
return (
<Layout>
<Head>
<title>🌸</title>
</Head>
<section className={utilStyles.centerSection}>
<button>Shade loving plants</button>
<button>Sun loving plants</button>
</section>
{items.map((item) => (
<ul key={item._id.oid}>
<li className={utilStyles.listItem}>
<Link href='/flowers/[flowerid]' as={`/flowers/${item._id}`}>
<a className={utilStyles.list}>
<h2 className={utilStyles.headingTop}>{item.common_name}</h2>
<img className={utilStyles.imgList} src={`${item.cover_image}`} alt={item.common_name} />
</a>
</Link>
</li>
</ul>
))
}
</Layout>
)
}
This is pages/[flowerid].js
import React, { useEffect, useState } from 'react'
import Head from 'next/head'
import Router from 'next/router'
import axios from 'axios'
import Layout from '../components/layout'
import utilStyles from '../styles/utils.module.css'
const FlowerDetail = () => {
const [flower, setFlower] = useState(null)
useEffect(() => {
const fetchData = async () => {
const { flowerId } = Router.query
try {
const { data } = await axios.get(
`https://flowers-mock-data.firebaseio.com/flowers/${flowerId}.json`
)
console.log(`blomma`, data)
setFlower(data)
} catch (error) {
console.log(`Can not find id`, error)
}
}
fetchData()
}, [])
if (!flower) return <div>Loading...</div>
console.log('no flowers to see')
return (
<Layout>
<Head>
<title>🌸</title>
</Head>
<div>
<p>Blooming season {flower.blooming_season}</p>
<img className={utilStyles.imgList} src={`${flower.cover_image}`} alt={flower.common_name} />
<p>Common name {flower.common_name}</p>
<h5>{flower.notes}</h5>
</div>
</Layout>
)
}
export default FlowerDetail;
The problems holding you back...
The _id coming from your API is an object which contains an oid property:
"_id": {
"oid": "5e57880c9fa50a095f475c18"
},
Therefore you'll want to use the items._id.oid property as a link source. As is, you're passing the id object to the link source, which is why your links are invalid: http://localhost:3000/flowers/[object%20Object]
Unfortunately, that's not the only problem. The other problem is that the current data structure doesn't contain any reference to the URL parameter id (where 1.json is supposed to reference oid: 5e579cdfe99bdd0ca1cf64a3, 2.json references oid: 5e579dfcb664120cd14b4331, and so on), so you'll need to reconfigure your API data to include a property with this URL id within each data document (id: 1, id: 2, ...etc) and then use that id as your link source OR you'll have to reconfigure your API to utilize this oid property as a URL parameter to match against (instead of 1.json it'll use the oid as a URL parameter to match against and be 5e579cdfe99bdd0ca1cf64a3.json).
For example, this API endpoint (which comes from http://jsonplaceholder.typicode.com) contains an id property that is used as a URL parameter to retrieve the specific JSON file by its id (notice how both the URL and the JSON data contain an id of 9).
On a side note, if you're using Chrome, you can install this extension to prettify/format JSON, so it looks like this.
OID doesn't match the requested API endpoint
For reference, here's how your response is currently structured when you query this endpoint (notice that the 9 in https://flowers-mock-data.firebaseio.com/flowers/9.json isn't referenced/used anywhere within the JSON data, so using the oid (https://flowers-mock-data.firebaseio.com/flowers/5e57a0e1e5f2470d789335c2.json) as a parameter won't be valid):
{
"__v": {
"numberInt": "0"
},
"_id": {
"oid": "5e57a0e1e5f2470d789335c2"
},
"blooming_season": "Late Spring",
"common_name": "Ornamental onion",
"cover_image": "https://images.unsplash.com/photo-1565685225009-fc85d9109c80?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1950&q=80",
"depth": {
"numberInt": "3"
},
"height": [
{
"numberInt": "6"
},
{
"numberInt": "60"
}
],
"latin_name": "Allium",
"notes": "Usually pest-free; a great cut flower",
"soil": [
"well drained",
"moist",
"fertile"
],
"spacing": {
"numberInt": "12"
},
"sun": true
}
Example fetch data repo
I've put together an example with some comments that incorporates the API endpoint (jsonplaceholder) mentioned in the paragraph above, as well as implementing some of nextjs's data fetching methods (excluded getStaticPaths for simplicity; however, technically, it can replace getServerSideProps in the dynamic pages/users/[id].js page since the data will never be changed/updated during runtime):
fetch data example repo

cannot access value passed from context api in firestore fetch data function

so I am trying to fetch some data from my firestore through the following bit of code
import React, { useContext, useState, useEffect } from 'react';
import { CocktailContext } from '../../context/CocktailContext';
import fire, { db } from '../../Config/firebase';
import RecipeCard from '../RecipeCard/RecipeCard';
import NavBar from '../NavBar/NavBar';
export default function SavedItemPage() {
const [ content, setContent ] = useState(null);
const { uid } = useContext(CocktailContext);
useEffect(() => {
listenForMessages(uid)
}, []);
const listenForMessages = (id) => {
db.collection('users').doc(id).onSnapshot(function(doc) {
const allMessages = [];
allMessages.push(doc.data());
setContent(allMessages);
console.log(content);
});
};
return (
<div>
<NavBar />
</div>
);
}
what should basically happen here is that the context API which looks like below, takes the uid and feeds it into listenForMessages() which then does the job of fetching the content
{
"name": "State",
"value": null,
"subHooks": []
},
{
"name": "Context",
"value": {
"recipes": "[{…}, {…}, {…}, {…}, {…}, {…}]",
"searchTerm": "ƒ searchTerm() {}",
"updateUid": "ƒ updateUid() {}",
"uid": "MfLXrE5czaYK7fYiTFuqFv9SZV02"
},
"subHooks": []
},
{
"name": "Effect",
"value": "ƒ () {}",
"subHooks": []
}
]
trouble is that when the page loads i am met with this errors- FirebaseError: Function CollectionReference.doc() requires its first argument to be of type non-empty string, but it was: ""
so the trouble is that listenForMessages() is not able to access the uid value in the context API.
Can anyone tell me how to solve this issue?
I don't know how Firebase works, but the useContext maybe is incorrect ?
usually you use it this way :
[state, dispatch] = useContext(CocktailContext)
Which will give you the whole state. In the state you provided there is 3 objects in your state so i guess you will access the uid by doing something like:
useEffect(() => listenForMessages(state[1].value.uid));
I see there is a ' ] ' closing your provided state but nothing at the beginning, just adapt in case i miss a node level.

React-Hooks: Fetching data results into empty 'hits' array

I'm trying to fetch data with React Hooks. It all seems to work but the hits array is empty even though the data is fetched correctly.
Here's my code:
import React, { useState, useEffect } from 'react';
import MUIDataTable from "mui-datatables";
import { createMuiTheme, MuiThemeProvider, withStyles } from '#material-ui/core/styles';
export default function Dashboard(props) {
var classes = useStyles();
var theme = useTheme();
// local
var [mainChartState, setMainChartState] = useState("monthly");
const [data, setData] = useState({ hits: [] });
const url = my_url;
useEffect(() => {
const fetchData = async () => {
const result = await axios(
url,
);
setData(result.data);
console.log(result.data);
console.log(data);
};
fetchData();
}, []);
return (
<Grid item xs={12}>
<MuiThemeProvider theme={getMuiTheme()}>
<MUIDataTable
title="Analyzed DAOs"
data={data.hits}
columns={["Name", "Members", "Proposals", "Voters"]}
options={{
filterType: "checkbox",
}}
/>
</MuiThemeProvider>
</Grid>
)
}
When printing out the result.data, I get an array with 5 objects (as it should be) but when printing out the data.hits the result is am empty array, and the table shows zero rows.
What am I missing? Probably a lifecycle issue, but how do I fix it?
I'm the OP. Looks like for the code, as written in my question, to work, my data needs to be wrapped with a bit of json.
My data, as it comes from the server, is a json array. To make it work I did the following:
var jsonData = {};
jsonData.hits = result.data;
setData(jsonData);
That's it. Now it all works. It's a workaround and there's probably a more elegant solution.
Setting setData({ hits: [result.data] }) will allow your result to be assigned to data.hits.
setData will completely override any default or current value for data.
Actually your code works as expected, I guess you just need to keep the MUIDataTable columns matches the properties and the data structure that returned from your API.
Assume your returned data look like this:
const data = [
{
name: "Joe James",
company: "Test Corp",
city: "Yonkers",
state: "NY"
...
},
// more objects
];
You will need to set your columns like this:
const columns = [
{
name: "name", // This should match the data property
label: "Name", // Label will be shown
options: {...}
},
{
name: "company",
label: "Company",
options: {...}
},
{
// More columns for `city` and `state` etc...
}
Please follow this codeSandbox example.

Resources