How to dynamically load a Vue component after using require.context? - javascript

Currently I am loading all of my Vue components with require.context, this searches my components directory with a regex for .vue files. This works fine but I would like to load async components as well with dynamic imports.
Currently when I use require.context all files get loaded so even If I want to use a dynamic import my file is already loaded and nothing happens.
I need a way to exclude certain files from my require.context call. I cannot dynamically create a regex because this does not work with require.context.
// How I currently load my Vue components.
const components = require.context('#/components', true, /[A-Z]\w+\.vue$/);
components.keys().forEach((filePath) => {
const component = components(filePath);
const componentName = path.basename(filePath, '.vue');
// Dynamically register the component.
Vue.component(componentName, component);
});
// My component that I would like to load dynamically.
Vue.component('search-dropdown', () => import('./search/SearchDropdown'));
It seems the only way to do this is either manually declare all my components, which is a big hassle.
Or to create a static regex that skips files that have Async in their name. Which forces me to adopt a certain naming convention for components that are async. Also not ideal.
Would there be a better way to go about doing this?

const requireContext = require.context('./components', false, /.*\.vue$/)
const dynamicComponents = requireContext.keys()
.map(file =>
[file.replace(/(^.\/)|(\.vue$)/g, ''), requireContext(file)]
)
.reduce((components, [name, component]) => {
components[name] = component.default || component
return components
}, {})

Works with Vue 2.7 and Vue 3.
The lazy mode forces requireContext to return a promise.
const { defineAsyncComponent } = require('vue')
const requireContext = require.context('./yourfolder', true, /^your-regex$/, 'lazy')
module.exports = requireContext.keys().reduce((dynamicComponents, file) => {
const [, name] = file.match(/^regex-to-match-component-name$/)
const promise = requireContext(file)
dynamicComponents[name] = defineAsyncComponent(() => promise)
return dynamicComponents
}, {})
You can also use defineAsyncComponent({ loader: () => promise }) if you want to use the extra options of defineAsyncComponent.

Related

Vue 2.6 with Rollup: import component by variable name

I am trying to add dynamic routes to my vue router (using router.addRoute()). It works so far, but I get a problem as soon as I try to set a component for my dynamic route.
This is the code that works:
var name = "reports";
var path = "reports/foo";
var item = {
name: name,
path: path,
component: () => import( "../pages/reports/Reports_Foo.vue" )
};
When calling the page it correctly loads the content of my Reports_Foo.vue
But when I want to load the vue file dynamically, like this:
var filename = "Reports_Foo";
var name = "reports";
var path = "reports/foo";
var item = {
name: name,
path: path,
component: () => import( "../pages/reports/"+filename+".vue" )
};
It no longer works and I get the following javascript error:
TypeError: Failed to fetch dynamically imported module: https://localhost:123456/js/pages/reports/Reports_Foo.vue
Why? Do you know a way to fix this ?
Edit: I'm using rollup to convert the files into chunks and then reference them.
Try explicitly loading the default export:
var item = {
name: name,
path: path,
component: async () => (await import("../pages/reports/"+filename+".vue")).default
}

Interpolate env vars client side in gatsby react app

I am using Gatsby as a Static Site Generator and using Netlify to deploy.
Netlify lets you set Environment Variables in its UI backend.
I've set a few env vars in the Netlify backend to be able to post subscribers to a mailing list.
DATA_NO = 'XXXX'
LIST_ID = '123456'
API_KEY = 'XXXXXXXXXXXXXXXXXXXXXXXXXX'
In my src files, I've got a component that responds to a onSubmit event and constructs a URL to post a new subscriber.
(axios is used as a package for sending HTTP requests, etc)
import React, { useState } from "react"
import axios from 'axios'
const Form = () => {
const [userEmail, setState] = useState({'email_address': ''})
const creds = 'anystring:'+ process.env.API_KEY
let URL = 'https://'+ process.env.DATA_NO +'.api.example.com/3.0'
URL += '/lists/'+ process.env.LIST_ID +'/members'
const submitSubscribe = async e => {
e.preventDefault()
const payload = {
'email_address': userEmail.email_address,
'status': 'subscribed'
}
try {
const response = await axios.post( URL , payload, {
headers: {
'Authorization': 'Basic ' + Buffer.from(creds ).toString('base64')
}
})
console.log('r', response)
console.log('r data', response.data)
} catch(err) {
console.log(err);
}
}
return (
<form name="newsletter-signup" method="post" onSubmit={submitSubscribe}>
{/*<input type="hidden" name="form-name" value="newsletter-signup" />*/}
<input type="email" placeholder="Email required" onChange={handleChange} value={userEmail.email_address} required />
<button type="submit" className="button primary-button-inverted">Send'</button>
</form>
)
}
So, what's happening is that on RUN time, my env vars are coming out as undefined.
I've been on the Netlify docs and they keep saying you need to interpolate the values to the client to be able to use them. I understand the logic here. These env vars need to be printed and bundled during build time, not invoked at run time.
The question I'm struggling with is HOW do I do this?
I have set up a .env.development file in the root of my project. I have tried prefixing my env vars with GATSBY_ but I still have the same trouble.
I tried using require('dotenv').config() but I'm not sure where exactly to put that (in my gatsby-node.js, gatsby-config.js) or do I need to include on the page with my component that is using these env vars.
I'd like to be able to set these vars up in one place (maybe two if testing in development) but I don't want to much tweaking involved to be able to use them in both development and production builds.
I also understand that Netlify or Gatsby can process these vars into a functions/ folder in my source code that I can somehow make use of but that seems like more than I need to just post a simple form.
Please help!
Update
Current code:
In my project root, I created two .env files, one for development and one for production. They each share the following format (remember, I am developing in GatsbyJS):
GATSBY_MC_API_KEY="xxxxxxxxxxxxxxxxxxxxxxxxx-xxxx"
GATSBY_MC_DATA_NO="xxxx"
GATSBY_MC_AUDIENCE_ID="xxxxxxxxxxx"
I've set up a separate config.js file in src/config/config.js to organize and validate my env vars (thanks #Baboo_). It looks like:
export const MC_API_KEY = process.env.GATSBY_MC_API_KEY;
export const MC_DATA_NO = process.env.GATSBY_MC_DATA_NO;
export const MC_AUDIENCE_ID = process.env.GATSBY_MC_AUDIENCE_ID;
const envVars = [
{name: "MC_API_KEY", value: MC_API_KEY},
{name: "MC_DATA_NO", value: MC_DATA_NO},
{name: "MC_AUDIENCE_ID", value: MC_AUDIENCE_ID}
]
export const checkEnvVars = () => {
const envVarsNotLoaded = envVars.filter((envVar) => envVar.value !== undefined);
if (envVarsNotLoaded.length > 0) {
throw new Error(`Could not load env vars ${envVarsNotLoaded.join(",")}`);
}
}
checkEnvVars()
However, when I run gatsby develop, the "Could not load env vars" error gets thrown.
You are doing it the right way.
What you have to do is indeed prefix your environment variables with GATSBY_, Gatsby will automatically load them. Then call them in your code:
const creds = 'anystring:'+ process.env.GATSBY_API_KEY
let URL = 'https://'+ process.env.GATSBY_DATA_NO +'.api.example.com/3.0'
tURL += '/lists/'+ process.env.GATSBY_LIST_ID +'/members'
Make sure to use the whole string process.env.GATSBY_LIST_ID instead of process.env[GATSBY_LIST_ID] because the object process.env is undefined.
Locally
Make sure to create to .env files, .env.development and .env.production. The former is used when you run gatsby develop and the latter when you run gatsby build.
You may already know that you shouldn't commit these files.
Netlify
Add the same environment variables in your deployment pipeline on Netlify. Here is the related doc. This way Netlify can build your webiste when being deployed.
Improvements
Instead of refering environment variables directly, create a file where they are loaded and if one of them cannot be retrieved, throw an error. This way you will be noticed when the loading fails and save debugging time.
Example:
// config.js
export const API_KEY = process.env.GATSBY_API_KEY;
export const DATA_NO = process.env.GATSBY_DATA_NO ;
const envVars = [
{name: "API_KEY", value: API_KEY},
{name: "DATA_NO", value: DATA_NO},
]
const checkEnvVars = () => {
const envVarsNotLoaded = envVars.filter(isUndefined);
if (envVarsNotLoaded.length > 0) {
throw new Error(`Could not load env vars ${envVarsNotLoaded.join(",")}`);
}
}
const isUndefined = (envVar) => typeof envVar.value === "undefined";
// component.js
import React, { useState } from "react"
import axios from 'axios'
// Import environment variables
import { API_KEY, DATA_NO } from "./config"
const Form = () => {
// ...
const [userEmail, setState] = useState({'email_address': ''})
const creds = 'anystring:'+ API_KEY
let URL = 'https://'+ DATA_NO +'.api.example.com/3.0'
You need to add a different env file for the two environments to make this work.
Meaning .env.development and .env.production.

Generate select option entries from variables in ReactJs

I am still not as fluent in ReactJS, Ant Design, Sagas and Reducers as I thought I am: I am trying to maintain/ debug a single-page app and I am not sure which files I have to adjust and how.
The goal is to add the following <select> in their render() function within component.tsx:
<Select showSearch
style={{ width: 150 }}
placeholder="Select entity"
// onChange={(e) => { this.handleSelectorChange("Entity", e) }} // to tackle later
// value={this.props.selectedEntity.toString()} // ------ " ------
>
<Option value="0">Option 0</Option>
{this.props.availableEntities.map((myEntity) =>
(<Option key={myEntity.Id.toString()}
value={myEntity.Id.toString()}>{myEntity.Name}</Option>))}
</Select>
In the respective container.tsx, I added:
const mapStateToProps = (state ) => ({
availableEntities: GeneralSelectors.getAvailableEntities(state),
});
What I get back is the following is only Option 0 instead of all entities (and even on a wrong place), see screenshot
How do I get my selection options dynamically generated using data from an API? Since I do not see backend being called (using the Network tab of my Chrome debugger), I assume something is wrong with the sagas, but that is just an hypothesis.
Background info
For another single-page-app (SPA) of the same project which already has that <select> inside, I found a urls.ts which looks as follows
import UrlMap from "../UrlMap"
import * as Actions from "./actions"
import * as GeneralActions from "../general/actions"
export const urls = [
new UrlMap("myWorkingSPA",
() => [
Actions.load(),
GeneralActions.loadEntities(),
Actions.loadDifferencePositions()
])
];
I don't know where this file is actually called, and what I have to modify where in order to include it.
In the respective actions.ts of the running SPA I find
export function loadEntities() : Action {
return {
type: LOAD_ENTITIES
}
}
I am not sure whether my SPA is also taking this actions.ts or not.
The sagas.ts is the same for the running SPA and for my component:
function* loadEntities() {
const url = config.apiBaseUrl + "/api/Entities";
try {
const response: Response = yield fetch(url);
const json: Interfaces.Entity[] = yield response.json();
yield put(Actions.setEntities(json));
} catch (e) {
notification.error({ message: "Error", description: "Error loading Entities" });
}
}
function* watchLoadEntities() {
yield takeEvery(Actions.LOAD_ENTITIES, loadEntities);
}
Hope that was not too much information, but I guess it is all related to the problem.
References
populate select option in reactjs
Mapping checkboxes inside checkboxes ReactJS
Eventually I also found another sagas.ts which was also in effect:
function* parseUrl({ payload }) {
const pathName = payload.location.pathname;
const myComponentURI: match<{}> = matchPath(pathName, { path: config.uiBaseUrl + "myComponent" });
if (myComponentURI) {
yield put(GeneralActions.loadEntities()); // <---
}
// ... here are many more const. for various different pages
}
The GeneralActions refers to an actions.ts in a directory general.
I solved the issue by putting the marked line. Warning: I believe, this is not the way things should be implemented, I am just saying that it hot-fixed my issue.
As clafou pointed out in a comment, the clean way would be to trigger the yield on startup.

Dynamic Imports - NextJS

I have a simple function which loads a script:
const creditCardScript = (
onReadyCB,
onErrorCB,
) => {
let script = document.createElement("script");
script.type = "text/javascript";
script.src = process.CREDIT_CARD_SCRIPT;
document.head.appendChild(script);
script.onload = function() {
...
};
};
export default creditCardScript;
Before I migrated to NextJS, I was importing the script with: import creditCardScript from "./creditCardScript".
Sine NextJS renders components server side in Node, care needs to be taken to ensure that any code with a reference to window (which is browser specific), doesn't get called until componentDidMount.
NextJS solves this issue by providing dynamic imports (a wrapper around react-loadable) which:
only load the component when needed,
provides an option to only load the component on client side
(ssr: false).
I went ahead and implemented dynamic imports:
const creditCardScript = dynamic(import("./creditCardScript"), { ssr: false });
In componentDidMount:
componentDidMount = () => {
creditCardScript(
this.onReadyCB,
this.onErrorCB
);
};
But I'm getting this:
Uncaught TypeError: Cannot call a class as a function
I've tried to convert the function to a class and use the constructor to pass in args, but my code now fails silently.
As Neal mentioned in the comments, all I need to do is something like this in componentDidMount:
const { default: creditCardScript } = await import("./creditCardScript");
Link to the official tutorial
Export default only work with import from statement, you can try
export creditCardScript;
And on import, u can use like this
const {creditCardScript} = dynamic(import("./creditCardScript"), { ssr: false });

Vuex modules state is not updating on hot reload

I have setup hot reloading and dynamic loading of my vuex modules.
store.js file - hot update section
if (module.hot) {
// accept actions and mutations as hot modulesLoader
module.hot.accept([
'./store/modules/index.js',
'./store/helpers/global-actions',
'./store/helpers/global-mutations',
...modulePaths,
// './store/helpers/global-actions',
], () => {
let newModules = require('./store/modules').modules
store.hotUpdate({
actions: require('./store/helpers/global-actions'),
mutations: require('./store/helpers/global-mutations'),
modules: newModules,
})
})
}
modules/index.js file
const requireModule = require.context('.', true, /index.js$/)
const modules = {}
const modulePaths = []
requireModule.keys().forEach(fileName => {
if (fileName === './index.js') {
modulePaths.push(fileName.replace('./', './store/modules/'))
return
} else {
let moduleName = fileName.match(/(?<=\/)\w*(?=\/)/g)[0]
modulePaths.push(fileName.replace('./', './store/modules/'))
modules[moduleName] =
{
namespaced: false,
...requireModule(fileName),
}
}
})
export {modulePaths, modules}
Basically what this code does is loading folders with index.js file as modules (where module name is foldername) dynamically.
If I update module actions or getters or mutations everything works as expected I do get new actions added to store as well as mutations, when either of modules is updated.
The only thing I can't get to work is to get modules state changed on update. So if I change modules state it does not get reflected. Is it a normal behaviour? Or am I doing something wrong?

Categories