Proper way of passing asynchronous data in nextjs to pages - javascript

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,
},
};

Related

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

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',
},
};
};

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

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

Layout Break when using single-spa micro frontend with vue and reactjs

I have a problem in my single-spa project, I don't know why but sometimes my single-spa break the layout like sometimes header in the bottom and the content above it, or the content below the footer.
for you who doesn't know what single-spa, you can read in here: https://single-spa.js.org/docs/examples
this is my registration apps:
import { registerApplication, start } from "single-spa";
import * as isActive from "./activity-functions";
registerApplication(
"#vue-mf/vue-navbar",
() => System.import("#vue-mf/vue-navbar"),
isActive.vueNavbar
);
registerApplication(
"#vue-mf/rate-dogs",
() => System.import("#vue-mf/rate-dogs"),
isActive.vueComponent
);
registerApplication(
"#react-mf/people",
() => System.import("#react-mf/people"),
isActive.reactComponent
);
registerApplication(
"#vue-mf/vue-footer",
() => System.import("#vue-mf/vue-footer"),
isActive.vueFooter
);
start();
and this is my activity function:
export function prefix(location, ...prefixes) {
return prefixes.some(
prefix => location.href.indexOf(`${location.origin}/${prefix}`) !== -1
);
}
export function vueNavbar(location) {
// The navbar is always active
return true;
}
export function vueComponent(location) {
return prefix(location, "rate-doggos");
}
export function reactComponent(location) {
return prefix(location, "people");
}
export function vueFooter(location) {
// The footer is always active
return true;
}
for better visualization, this is the example of the layout break:
I'm using single-spa with vue & react
can someone help me to solve this? I'm quite confused about this layout break
I had the same issue and I would guess that this is the result of asynchronously loading each of the apps and that the render order would depend on which app was returned first.
It looks like Joel Denning realized this and has created a Layout Engine to deal with it: https://single-spa.js.org/docs/layout-overview.
However, it's still in beta and I'm finding that there's still some limitations with it but perhaps it would suit your needs.

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.

ElectronJS - sharing redux store between windows?

I have an electron app based on electron-react-boilerplate.
Now, that I have one window running as I wanted it to run, I started to create a new window.
I currently have 2 html files - one for each window - containing div roots:
<div data-root id="main_root"></div>
<div data-root id="second_root"></div>
My index.js file that is response for rendering the react app looks like this:
import React from 'react';
import { render } from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import HomeRoot from './roots/HomeRoot';
import HoverRoot from './roots/HoverRoot';
import { configureStore, history } from './store/configureStore';
const store = configureStore();
const rootMapping = {
main_root: {
name: 'HomeRoot',
Component: HomeRoot,
getNextRoot: () => require('./roots/HomeRoot'),
},
second_root: {
name: 'SecondRoot',
Component: SecondRoot,
getNextRoot: () => require('./roots/SecondRoot'),
},
};
const renderDesiredRoot = () => {
const rootElementID = document.querySelector('[data-root]').id;
const root = rootMapping[rootElementID];
if (!root) throw Error('There is no such Root component!');
const { Component, getNextRoot, name } = root;
render(
<AppContainer>
<Component store={store} history={history} />
</AppContainer>,
document.getElementById(rootElementID),
);
if (module.hot) {
module.hot.accept(`./roots/${name}`, () => {
const NextRoot = getNextRoot();
render(
<AppContainer>
<NextRoot store={store} history={history} />
</AppContainer>,
document.getElementById(rootElementID),
);
});
}
};
renderDesiredRoot();
What it does, it checks which div root is available, and renders proper components.
My problem
How can I make a store that will be shared accross the BrowserWindow instances? I already looked into 2 npm packages (electron-redux and redux-electron-store) and they do not seem as a solution for me in this case.
I tried using this very simple approach, it works almost perfectly, but sometimes it's freezing (I'm not sure yet what exactly is making it to freeze). Maybe this could be useful to anyone, and if someone finds out what is causing the freezing issue, please let us know.
Redux store code (this same code is used by all windows):
export const store = window.opener?.store || createStore(...);
Object.assign(window, { store });
Then I need to open new electron window from a renderer process of the main window using:
const newWindow = window.open("/path", "someName");
And we also need this code on the main process:
win.webContents.on("new-window", function (e, url, frameName, _, options) {
e.preventDefault();
if (frameName === "someName")
e.newGuest = new BrowserWindow({ ...options, width: 300, height: 200, /* anything else you wanna add */ });
});
Nice solution:
You can use redux-state-sync which is a redux middleware that used to synchronize the redux store and actions across multiple react tabs, which works nicely with electron as the tabs here are the different renderer process.
The only hindrance is to initialize the store in the newly created window, that can be done by sending the store state with an ipc call from the main window that opens the new one, that dispatches an action to update the state.
This initialization approach works nicely in react#17.0.0 , but for some reason it doesn't in react react#18.0.0

Categories

Resources