Next.js - Dynamically import component library depending on getServerSideProps response - javascript

I have a project with multi-tenancy and want to have the possibility to switch between component libraries based on an external response while keeping SSR.
Both component libraries are having the exact same structure, with the same exported components and accepted props.
Normally, we could do something like this:
import * as Components from '#acme/components-old';
export default function Page() {
return <Components.Button>Some Awesome Button</Components.Button>;
}
Now I want to have something like this (pseudo-code):
export default function Page({ components }) {
import * as Components from '#acme/components-' + components;
return <Components.Button>Some Awesome Button</Components.Button>;
}
export const getServerSideProps = async () => {
return {
props: {
components: 'old',
},
};
};
I'm using styled-components for styling. It seems like I cannot use next/dynamic as it does not work with template literals and using a normal import statement also does not work since it requires an async function which seems to break SSR. Also, in the future I'd love to have more than only 2 templates without loading all templates in the client. Unused Templates should be tree-shaken.

You can import both libraries and create a map of your own. As this is on server-side you will only pass to your client what they need.
import * as OldComponents from '#acme/components-old';
import * as NewComponents from '#acme/components-new';
const COMPONENT_LIBS_MAP = {
old: OldComponents,
new: NewComponents,
}
export default function Page({ components }) {
const Components = COMPONENT_LIBS_MAP[components];
return <Components.Button>Some Awesome Button</Components.Button>;
}
export const getServerSideProps = async () => {
return {
props: {
components: 'old',
},
};
};

Related

Proper way of passing asynchronous data in nextjs to pages

current directory setup:
- components
- NavBar
- Header
- Layout
- pages
- pages
- demo.js
- _app.js
- index.js
// index.js
import React from 'react';
import NewLayout from "../../components/NewLayout/NewLayout.js";
import $nacelle from '../../services/nacelle';
const Index = ({page}) => (
<>
<NewLayout header={page} />
<pre>{JSON.stringify(page.id, null, 2)}</pre>
</>
);
export async function getStaticProps({ context }) {
try {
const page = await $nacelle.data.content({
handle: 'header_us_retail',
type: 'header'
});
return {
props: { page }
};
} catch {
// do something useful if it doesnt work
const page = 'failed';
return {
props: { page }
};
}
}
export default Index;
I am importing Layout into the index.js file, loading asynchronous data and passing it to layout as props that will then be used to render the header and navbar (which are imported by the layout component). This works as expected in the index file, however I want this same functionality to work in the demo.js file and any other file I create in pages or elsewhere. Most likely the issue is how I'm trying to use Nextjs and React (new to both), any help would be greatly appreciated.
Turns out the issue was with how Nacelle was accessing the environment variables, so not a NextJS, or React issue.
According to the devs there are multiple ways to expose the environment variables and the following method solved my particular issue:
// file in root: next.config.js
module.exports = {
env: {
NACELLE_SPACE_ID: process.env.NACELLE_SPACE_ID,
NACELLE_GRAPHQL_TOKEN: process.env.NACELLE_GRAPHQL_TOKEN,
NACELLE_ENDPOINT: process.env.NACELLE_ENDPOINT,
},
};

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

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 Export Variables with a Dynamic Names

I have a components folder in nuxt.js
/components/atoms/
and inside that folder I have an index.js to export all components dynamically
const req = require.context('./', true, /\.vue$/)
const components = {}
req.keys().forEach(fileName => {
const componentName = fileName.replace(/^.+\/([^/]+)\.vue/, '$1')
components[componentName] = req(fileName).default
})
export const { ButtonStyled, TextLead, InputSearch } = components
so I can import perfectly as I wish
import { ButtonStyled } from "#/components/atoms"
the problem is that I am defining the variables to be exported statically, fixed, so for each created component I would need to add another variable manually
I need to dynamically export the variable name
Example:
DynamicCreation = ['ButtonStyled', 'TextLead', 'InputSearch']
export const { DynamicCreation } = components
// output -> export const { ButtonStyled, TextLead,InputSearch } = components
I need to export the name of already unstructured variables
Note: I can not use this export default components because I can not import like this import { ButtonStyled } from "#/components/atoms"
You should be able to do it like this:
export default components
Then in your file where you want to use the components:
import * as components from '#/components/atoms'
Then when you need to use the components in your Vue files, you need to map them:
#Component({
components: {
ButtonStyled: components.ButtonStyled
}
})
And now you have:
<ButtonStyled></ButtonStyled>
You can make something like this way, check if is what do you need.
Create a file to import a conjunct of components: allComponents.js
export default {
componentOne: require('./passToOneComponent.js');
componentTwo: require('./passToOneComponent.js');
componentThree: require('./passToOneComponent.js');
}
After in index.js export the allComponents.js with the name that you wish:
export {default as SomeName } from 'allComponents.js';
So in the final file, you can make something like:
import { SomeName } from 'index.js';
SomeName.componentOne();
I created a library that does this type of export, anyone who wants can install via npm
I created a Webpack Plugin that makes named exports from a component, maybe this helps other people
Weback Plugin - named-exports

ReactJS: re-base helper filer for collection method calls

I have struggled a lot trying to figure out how to combine ReactJS and firebase (3).
Luckily I found the awesome re-base repo, which is awesome and exactly what I want.
fb-config.js
var Rebase = require('re-base');
var base = Rebase.createClass({
apiKey: "apiKey",
authDomain: "projectId.firebaseapp.com",
databaseURL: "https://databaseName.firebaseio.com",
storageBucket: "bucket.appspot.com",
messagingSenderId: "xxxxxxxxxxxxxx"
});
export default base;
app.js
import React, { Component } from 'react';
import base from 'fb-config';
import ExampleComponent from './components/ExampleComponent';
class App extends Component {
constructor() {
super();
// getInitialState
this.state = {
DB: {}
};
}
componentDidMount() {
this.ref = base.syncState('demo',
{ context: this,
state: 'DB' }
);
}
componentWillUnmount() {
base.removeBinding(this.ref);
}
render() {
return (
<ExampleComponent />
)
}
}
export default App
components/ExampleComponent.js
import React, { Component } from 'react';
import base from '../fb-config';
class ExampleComponent extends Component {
constructor(props, context){
super(props, context);
this.pushing = this.pushing.bind(this);
}
pushing() {
const now = new Date().getTime();
let data = {};
data[now.toString(16)] = 'Hello World';
base.fetch('demo', {
context: this,
asArray: false
}).then( snapshot => {
data = {... snapshot, ...data};
base.post('demo', {data})
.then( () = { console.log("pushed") });
});
}
render() {
return (
<button onClick={this.pushing}>PUSH</button>
)
}
};
export default ExampleComponent;
This setup works. But I would like to move the pushing method to another file (like fb-commands.js). How would I do that?
You can move pushing as-is, all you need is to pass context to the function, since it wont have access to the component instance via this.
// fb-commands.js
export const pushing = (context) => {
const now = new Date().getTime();
let data = {};
data[now.toString(16)] = 'Hello World';
base.fetch('demo', {
context: context,
asArray: false
}).then( snapshot => {
data = {... snapshot, ...data};
base.post('demo', {data})
.then(() => console.log("pushed"));
});
};
// components/ExampleComponent.js
import React, { Component } from 'react';
import base from '../fb-config';
import {pushing} from 'path/to/fb-commands';
class ExampleComponent extends Component {
render() {
return (
<button onClick={() => pushing(this)}>PUSH</button>
);
}
}
export default ExampleComponent;
Based on my experience, I can think of 3 approaches (maybe there are more).
Approach #1: Via ES6 Import / Export (Simple)
ArneHugo describes it well in his answer.
Just export pushing function to a module and then import it and use it directly. Pass the this context or any other params needed.
Sounds awesome to me if you want to share utility functions.
Approach #2: Via Mixins.
That's not a good practice.
Plus, mixins are not supported when using React components written in ES6 syntax. Moreover, they will not have support for ES6 classes in React. The reason is that they are considered harmful.
So, mixins are on their way out.
Approach #3: Via Composition (Advanced)
That is the best option if you need to implement something more advanced then just re-using common methods. Let's say if you want:
to reuse logic and bootstrap abstraction
to render Highjacking
to do State abstraction and manipulation
to do Props manipulation
Then, enter Composition and Higher-Order Components!
A higher-order component is just a function that takes an existing component and returns another component that wraps it.
You can use this approach to wrap the component and not only share common code / logic, but share data, manipulate state, props and etc.
More on this topic you can read on the franleplant's great post here.
Conclusion
If you need just a helper method abstraction - use Approach #1.
Never use Approach #2.
If you want something more then that - consider applying Approach #3.

Categories

Resources