Load function from external script using #loadable/component in React - javascript

I have a JSON file with several filepaths to scripts that I want to be able to load dynamically into my React app, to build each component based on specifications that are in the metadata. Currently I have the metadata in my app as a Metadata data object.
metadata.json:
{
"component1": { "script": "./createFirstLayer.js" },
"component2": { "script": "./createSecondLayer.js" }
}
Each script exports a function that I want to be able to use to construct the component. For troubleshooting purposes, it currently only returns a simple message.
function createFirstLayer(name) {
return name + " loaded!";
}
export default createFirstLayer;
I did some research and identified the #loadable/component package. Using this package as import loadable from "#loadable/component";, I attempted to load my script into App.js like this:
async componentDidMount() {
Object.keys(Metadata).forEach(function(name) {
console.log(Metadata[name].script);
var createLayer = loadable(() => import(Metadata[name].script));
var message = createLayer(name);
console.log(message);
});
}
Everything I have tried throws the TypeError createLayer is not a function. How can I get the function loaded?
I have also attempted the lazy method.
I have recreated a working demo of my problem here.
EDIT: I have tried to put this at the top of my app
const scripts = {};
Object.keys(Metadata).forEach(async function(name) {
import(Metadata[name].script).then((cb) => scripts[name] = cb);
});
This causes the TypeError Unhandled Rejection (Error): Cannot find module './createFirstLayer.js'. (anonymous function)
src/components lazy /^.*$/ groupOptions: {} namespace object:66
I have also attempted
const scripts = {};
Object.keys(Metadata).forEach(async function(name) {
React.lazy(() => import(Metadata[name].script).then((cb) => scripts[name] = cb));
});
My goal is to be able to call the appropriate function to create particular layer, and match them up in the metadata.

You don't need #loadable/component for two reasons.
You can accomplish your goal with dynamic imports
'#loadable/component' returns a React Component object, not your function.
To use dynamic imports simply parse your JSON the way you were, but push the call to the import's default function into state. Then all you have to do is render the "layers" from within the state.
Like this:
import React, { Component } from "react";
import Metadata from "./metadata.json";
class App extends Component {
constructor(props) {
super(props);
this.state = { messages: [] };
}
async componentDidMount() {
Object.keys(Metadata).forEach(name=> import(`${Metadata[name].script}`).then(cb =>
this.setState((state, props) => ({ messages: [...state.messages, cb.default(cb.default.name)] }))));
}
render() {
return (
<div className="App">
{this.state.messages.map((m, idx) => (
<h1 key={idx}>{m}</h1>
))}
</div>
);
}
}
export default App;
Here is the working example

Related

How do I access app.state from a Cypress test in a Remix project

Cypress has a way to expose the app's state to the test runner -- in React it usually looks like this:
class MyComponent extends React.Component {
constructor (props) {
super(props)
// only expose the app during E2E tests
if (window.Cypress) {
window.app = this
}
}
...
}
Then you could access your state in a test with
cy.window()
.its('app.state')
.should('deep.equal', myStateObject)
However, the setup for Remix projects relies on functional components. I've tried this in my root.tsx component with a useEffect call:
export default function App() {
useEffect(() => {
window.app = App;
}, []}
}
as well as in the root route (routes/index.tsx) by importing the <App /> component and using the logic in the useEffect function above. Neither of these options are working and I'm not sure where else to go here. Remix's GitHub issues are devoid of questions about this issue, so maybe I'm going about this the wrong way. Any help is appreciated! Thanks!
I haven't done much work with Remix, but there is a question here that might be useful:
React - getting a component from a DOM element for debugging.
Note the last paragraph
Function components
Function components don't have "instances" in the same way classes do, so you can't just modify the FindReact function to return an object with forceUpdate, setState, etc. on it for function components.
That said, you can at least obtain the React-fiber node for that path, containing its props, state, and such. To do so, modify the last line of the FindReact function to just: return compFiber;
There's a lib cypress-react-app-actions that implements this for Cypress
export const getReactFiber = (el) => {
const key = Object.keys(el).find((key) => {
return (
key.startsWith('__reactFiber$') || // react 17+
key.startsWith('__reactInternalInstance$') // react <17
)
})
if (!key) {
return
}
return el[key]
}
// react 16+
export const getComponent = (fiber) => {
let parentFiber = fiber.return
while (typeof parentFiber.type == 'string') {
parentFiber = parentFiber.return
}
return parentFiber
}
One of the example tests is
/// <reference types="cypress" />
import { getReactFiber, getComponent } from '../support/utils'
it('calls Example double()', () => {
cy.visit('/')
cy.get('.Example').within(() => { // select via className of component
cy.contains('[data-cy=count]', '0')
cy.get('[data-cy=add]').click().click()
cy.contains('[data-cy=count]', '2')
cy.root().then((el$) => {
const fiber = getReactFiber(el$[0])
console.log(fiber)
const component = getComponent(fiber)
console.log(component.stateNode)
cy.log('calling **double()**')
component.stateNode.double() // work with component for functional
})
cy.contains('[data-cy=count]', '4')
})
})
This example is for class components, but given the info in Function components section above, you would use the component object rather than component.stateNode.

How do I load and run external Javascript code in React that have their definitions in the application and not in the imported file?

Basically, I'm trying to run a function that creates and adds a Recipe class to an array in React based on an external javascript file that is hosted online - but all the definitions are inside my React app.
The external file looks like (Recipes.js) this:
function LoadRecipes(){
AddToRecipes(new Recipe({
name: "Kronyxium Core",
components: [],
requirements: [],
craftedAt: "Frost Temple Smithy"
}));
}
The way I attempt to go on with this follows:
import React, {useState, useEffect} from 'react';
import RecipeManager from "../logic/RecipeManager.js";
const Recipe = RecipeManager.Recipe;
const recipesList = RecipeManager.recipesList;
const AddToRecipes = RecipeManager.AddToRecipes;
function RecipeController() {
const [loadingRecipes, setLoadingRecipes] = useState(true);
useEffect(() => {
const script = document.createElement("script");
script.src = "https://raw.githack.com/Soralei/extern/main/Recipes.js";
script.async = true;
script.onload = () => {
setLoadingRecipes(false);
}
document.body.appendChild(script);
}, []);
useEffect(() => {
if(!loadingRecipes){
window.LoadRecipes();
}
}, [loadingRecipes]);
return (
<div>
{loadingRecipes ? <p>Loading recipes...</p> : (
<>
<p>Recipes:</p>
{/*recipesList.map((a, index) => <p key={"r"+index}>{a.name}</p>)*/}
</>
)}
</div>
)
}
export default RecipeController
Note that I try to run the function using window.LoadRecipes() once the script has been imported. However, I get undefined errors when the function is run:
Recipes.js:3 Uncaught ReferenceError: AddToRecipes is not defined
at LoadRecipes (Recipes.js:3)
I'm also adding the content of RecipeManager.js for clarity. This is local logic, and the goal is to have the external function make use of it:
class Recipe{
constructor(options = {}){
this.name = options.name || "Unnamed Recipe";
this.components = options.components || [];
this.requirements = options.requirements || [];
this.craftedAt = options.craftedAt || "handcrafted";
}
}
const recipesList = [];
function AddToRecipes(Recipe){
recipesList.push(Recipe);
console.log(Recipe.name, "was added to the recipes list.");
}
const exported = {
Recipe: Recipe,
recipesList: recipesList,
AddToRecipes: AddToRecipes
}
export default exported;
Is this not possible, or am I just doing this entirely wrong?
Why am I doing this? The idea is to host the recipes online in a way that allows for other people to easily view, edit, and have the changes affect my app directly, while keeping most of the work in the React app.
You have to export the function to be able to access it.
Default export (only one per file):
function LoadRecipes(){
AddToRecipes(new Recipe({
name: "Kronyxium Core",
components: [],
requirements: [],
craftedAt: "Frost Temple Smithy"
}));
}
export default LoadRecipes; // export
You should import it like this:
import LoadRecipes from 'pathtofile';
Named export (multiple ones):
export function LoadRecipes() {
AddToRecipes(new Recipe({
name: "Kronyxium Core",
components: [],
requirements: [],
craftedAt: "Frost Temple Smithy"
}));
}
export const add (a, b) => a + b; // another one
Import like this (using { }):
import {
LoadRecipes,
add
} from 'pathtofile';
Named exports are useful to export several values. During the import, one will be able to use the same name to refer to the corresponding value. Concerning the default export, there is only a single default export per module. A default export can be a function, a class, an object or anything else. This value is to be considered as the “main” exported value since it will be the simplest to import.
You can read about JavaScript modules here

Onepage with Gatsby JS and Contentful, how to import my first simple string

I am trying to useStatic Query and GraphQL to get a simple title from
Contentful, pass it to state and then show in the render. I cant make it work. I am attaching an image showing my current setup and errors.
Possible problems: 1. the query returns an array, and I need to change it into a string, or access 0 element, the first one, because my content type is just one, as it is a onepage.
Placing of the query in the component, I am not sure if it can be in the constructor of an component
For comparison: in the screen from my file you can see a variable name showing Josh Perez, when I uncomment it and add it to this.state = { dataTest: name}, then in RENDER: this.state.dataTest returns the name Josh Perez well, so passing a variable to state works, but passing a string from graphql query is not possible for me...
I have a limitation which is that I need to create my page component with a class, because of the fact that in the Component did mount I am placing some JQuery, which works well for me.
THIS IS MY TEST CODE
1. In Constructor
class IndexPage extends React.Component {
constructor(props) {
super(props);
// this.state = { data: null };
const name = 'Josh Perez';
this.state = { dataTest: name };
}
In render
{this.state.dataTest}
This works, the variable name is passed to state and shown in render.
However, I want to show in this way a simple text string from Contentful. So I am trying code like this (error message is shown in the screens):
class IndexPage extends React.Component {
constructor(props) {
super(props);
// this.state = { data: null };
//const name = 'Josh Perez';
const data = useStaticQuery(graphql`
query {
allContentfulHomepage (limit: 1) {
edges {
node {
section1Title
}
}
}
}
`)
this.state = { dataTest: data };
It turns out, that the below suggested solution works. I am putting below
my attempt at callingfurther content. It does not work. It displays the following error "Cannot read property 'map' of undefined". I would be very grateful for a suggestion how to improve it, how to make it work.
export default class Test extends Component {
state = {
dataTest: this.props.data.test.edges.map(({ node: test }) =>
test.section1Title),
dataTest2: this.props.data.test.edges.map(({ node: test }) =>
test.section2Lead),
dataTest3: this.props.data.test.edges.map(({ node: test }) =>
test.section1Text.json)
}
render() {
return <div>
<h1>{this.state.dataTest}</h1>
<h1>{this.state.dataTest2}</h1>
{documentToReactComponents(this.state.dataTest3)}
</div>
}
}
export const query = graphql`
{
test:allContentfulHomepage(limit: 1) {
edges {
node {
section1Title
section2Lead
section1Text {
json
}
}
}
}
}
`
If you're writing a page component as a class, you don't need to use the UseStaticQuery, you can use the simple PageQuery for this purpose.
To loop through arrays, the map() method works as well.
UPDATE
import React, { Component } from 'react';
import { graphql } from 'gatsby';
export default class Test extends Component {
render() {
const { edges } = this.props.data.test;
return (
<div>
{edges.map(({ node: itemFromContentful }) => (
<h1>{itemFromContentful.section1Title}</h1>
<h1>{itemFromContentful.section2Lead}</h1>
{documentToReactComponents(section1Text.json)}
))}
</div>
);
}
}
export const query = graphql`
{
test:allContentfulHomePage(limit: 1) {
edges {
node {
section1Title
}
}
}
}
`
Whats happening:
The GraphQL query you're using is bringing the data you want from the Contentful;
The React Stateful Component (class Test) is receiving all the data available from the query as a prop;
We're accessing this data on the render() method using the destructing assignment;
we're accessing the data nodes through the map method (the one I suggested you to take a look;
The curly braces into the JSX allows you to use JS to manipulate what you want - In this case, to render the information.

React dynamic import does not accept string variable

I have a component which enables me to import images dynamically in react. This is the component I am using:
import React from "react";
import PropTypes from "prop-types";
// Lazily load an iamge
class LazyImageLoader extends React.Component {
constructor() {
super();
this.state = {
module: null,
};
}
async componentDidMount() {
try {
const { resolve } = this.props;
const { default: module } = await resolve();
this.setState({ module });
} catch (error) {
this.setState({ hasError: error });
}
}
componentDidCatch(error) {
this.setState({ hasError: error });
}
render() {
const { module, hasError } = this.state;
if (hasError) return <div>{hasError.message}</div>;
if (!module) return <div>Loading module...</div>;
if (module) return <img src={module} alt="Logo" />;
return <div>Module loaded</div>;
}
}
LazyImageLoader.propTypes = {
resolve: PropTypes.func.isRequired,
};
export default LazyImageLoader;
Now, if I try to use this compoent like this with a string to the image which should get imported it works perfectly fine:
<LazyImageLoader resolve={() => import("assets/images/os/netboot.svg")} />
But as soon as I extract the URL into a seperate variable it no longer works and I get the error message "cannot find module ...":
const path = "assets/images/os/netboot.svg";
<LazyImageLoader resolve={() => import(path)} />
Is there a way I can use variables for a dynamic import?
According to the answer in:
Dynamic imports in ES6 with runtime variables
"The rules for import() for the spec are not the same rules for Webpack itself to be able to process import()".
So no you can't use variables with webpack dynamic import statements.
It doesn't work directly to use the variable but could be used as follows -
const path = './test.jpg';
import(`${path}`);
You can do it with Vite. The example they cite uses a template string literal, but that shouldn't obligate you to use a prefix or something. You probably would anyway. Here is the docs and their example.
https://vitejs.dev/guide/features.html#dynamic-import
const module = await import(`./dir/${file}.js`)
It also seems like, with glob imports also described on that docs page, you have lots of other options to do it lots of ways.

How to load images asynchronously with webpack

I am attempting to load an image dynamically based on props in a react component.
So far this is what I have come up with:
MyComponent.jsx (uses ES7 property initializers syntax)
//...
const getIcon = (iconName) => {
require.ensure([], require => {
cb = () => require(`../../images/icons/${iconName}`)
})
}
class MyComponent extends Component {
state = {
categoryIcon: getIcon.bind(this, this.props.icon)
};
render() {
let iconStyle = {backgroundImage: `url('${this.state.categoryIcon}')`}
return(
<div className={styles.categoryIcon}
style={iconStyle}
></div>
)
}
}
Executing it produces this error:
GET http://localhost:3000/function%20()%20%7B%20[native%20code]%20%7D 404 (Not Found)
So, my question has multiple parts.
Is loading images asynchronously possible using require.ensure() with webpack?
If so, how?
If not...
Can I load images asynchronously with webpack?
If so, how?
Based on my example is there anything else I should consider?
I solved this with the following code:
(iconName) => {
require.ensure([], require => {
require(`../../images/icons/${iconName}.svg`)
})
}
class MyComponent extends Component {
state = {
categoryIcon: require(`../../images/icons/${this.props.icon}.svg`)
};
Reading the webpack docs on code splitting more carefully helped.

Categories

Resources