Dynamic Importing of an unknown component - NextJs - next.js

I want to load a component dynamically based on the route. I'm trying to make a single page which can load any individual component for testing purposes.
However whenever I try to do import(path) it shows the loader but never actually loads. If I hard code the exact same string that path contains then it works fine. What gives? How can I get nextjs to actually dynamically import the dynamic import?
// pages/test/[...component].js
const Test = () => {
const router = useRouter();
const { component } = router.query;
const path = `../../components/${component.join('/')}`;
console.log(path === '../../components/common/CircularLoader'); // prints true
// This fails to load, despite path being identical to hard coded import
const DynamicComponent = dynamic(() => import(path), {
ssr: false,
loading: () => <p>Loading...</p>,
});
// This seems to work
const DynamicExample = dynamic(() => import('../../components/Example'), {
ssr: false,
loading: () => <p>Loading...</p>,
});
return (
<Fragment>
<h1>Testing {path}</h1>
<div id="dynamic-component">
<DynamicComponent /> <!-- this always shows "Loading..." -->
<DynamicExample /> <!-- this loads fine. -->
</div>
</Fragment>
);
};
export default Test;

I put dynamic outside of the component, and it work fine.
const getDynamicComponent = (c) => dynamic(() => import(`../components/${c}`), {
ssr: false,
loading: () => <p>Loading...</p>,
});
const Test = () => {
const router = useRouter();
const { component } = router.query;
const DynamicComponent = getDynamicComponent(component);
return <DynamicComponent />
}

I had the same issue like the thread opener.
The Documentation describe, that it's not possible to use template strings in the import() inside dynamic:
In my case it was also impossible to add an general variable with the path there...
Solution
I've found an easy trick to solve this issue:
// getComponentPath is a method which resolve the path of the given Module-Name
const newPath = `./${getComponentPath(subComponent)}`;
const SubComponent = dynamic(() => import(''+newPath));
All the MAGIC seems to be the concatenation of an empty String with my generated Variable newPath: ''+newPath
Another Solution:
Another Solution (posted by bjn from the nextjs-Discord-Channel):
const dynamicComponents = {
About: dynamic(() => import("./path/to/about")),
Other: dynamic(() => import("./path/to/other")),
...
};
// ... in your page or whatever
const Component = dynamicComponents[subComponent];
return <Component />
This example might be useful, if you know all dynamically injectable Components.
So you can list them all and use it later on in your code only if needed)

The below code worked for me with dynamic inside the component function.
import dynamic from "next/dynamic";
export default function componentFinder(componentName, componentPath) {
const path = componentPath; // example : "news/lists"
const DynamicComponent = dynamic(() => import(`../components/${path}`),
{
ssr: false,
loading: () => <p>Loading Content...</p>,
});
return <DynamicComponent />;
}

It happens because router.query is not ready and router.query.component is undefined at the very first render of dynamic page.
This would print false at first render and true at the following one.
console.log(path === '../../components/common/CircularLoader');
You can wrap it with useEffect to make sure query is loaded.
const router = useRouter();
useEffect(() => {
if (router.asPath !== router.route) {
// router.query.component is defined
}
}, [router])
SO: useRouter receive undefined on query in first render
Github Issue: Add a ready: boolean to Router returned by useRouter

As it was said here before the dynamic imports need to be specifically written without template strings. So, if you know all the components you need beforehand you can dynamically import them all and use conditionals to render only those you want.
import React from 'react';
import dynamic from 'next/dynamic';
const Component1 = dynamic(() => import('./Component1').then((result) => result.default));
const Component2 = dynamic(() => import('./Component2').then((result) => result.default));
interface Props {
slug: string;
[prop: string]: unknown;
}
export default function DynamicComponent({ slug, ...rest }: Props) {
switch (slug) {
case 'component-1':
return <Component1 {...rest} />;
case 'component-2':
return <Component2 {...rest} />;
default:
return null;
}
}

Related

How to test computed value inside setup function in Vue.js 3 with vue-test-utils & Jest

I am getting "TypeError: Cannot add property myData, object is not extensible" on setData
Hello.vue
<template>
<div v-if="isEditable" id="myEditDiv">
<button type="button"> Edit </button>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, reactive} from "vue"
export default defineComponent({
setup() {
const myObject = {myName:"", myNumber:""}
let myData = reactive({myObject})
const isEditable = computed(() => {
return myData.myObject.myName.startsWith('DNU') ? false : true
})
return {
isEditable
}
}
})
</script>
Hello.spec.ts
import { shallowMount } from '#vue/test-utils'
import Hello from '#/components/Hello.vue'
import { reactive } from 'vue'
describe('Hello.vue Test', () => {
it('is isEditable returns FALSE if NAME starts with DNU', async () => {
const myObject = {myName:"DNU Bad Name", myNumber:"12345"}
let myData = reactive({myObject})
const wrapper = shallowMount(Hello)
await wrapper.setData({'myData' : myData})
expect(wrapper.vm.isEditable).toBe(false)
})
})
I also tried to see if that DIV is visible by:
expect(wrapper.find('#myEditDiv').exists()).toBe(false)
still same error. I might be completely off the path, so any help would be appreciated.
Update
This is possible several different ways. There's two issues that need to be addressed.
The variable has to be made available. You can use vue's expose function in setup (but getting the value is really messy: wrapper.__app._container._vnode.component.subTree.component.exposed😱) or just include it in the return object (accessible through wrapper.vm).
change how you mutate the data in the test.
your test has
const myObject = {myName:"DNU Bad Name", myNumber:"12345"}
let myData = reactive({myObject})
const wrapper = shallowMount(Hello)
await wrapper.setData({'myData' : myData})
even if setData was able to override the internal, it would not work.
the problem is that the setup function has this
let myData = reactive({ myObject });
const isEditable = computed(() => {
return myData.myObject.myName.startsWith("DNU") ? false : true;
});
where editable is using a computed generated from that instance of myData. If you override myData with a separate reactive, the computed will still continue to use the old one. You need to replace the contents of the reactive and not the reactive itself
To update the entire content of the reactive, you can use:
Object.assign(myReactive, myNewData)
you can make that a method in your component, or just run that from the test. If you update any value within the reactive (like myData.myObject) you can skip the Object.asign
Here are several versions of how you can test it.
Component:
<template>
<div v-if="isEditable" id="myEditDiv">
<button type="button">Edit</button>
</div>
</template>
<script>
import { computed, defineComponent, reactive } from "vue";
export default defineComponent({
setup(_, { expose }) {
const myObject = { myName: "", myNumber: "" };
let myData = reactive({ myObject });
const isEditable = computed(() => {
return myData.myObject.myName.startsWith("DNU") ? false : true;
});
const updateMyData = (data) => Object.assign(myData, data);
expose({ updateMyData });
return {
isEditable,
updateMyData,
myData
};
},
});
</script>
the test
import { shallowMount } from "#vue/test-utils";
import MyComponent from "#/components/MyComponent.vue";
const data = { myObject: { myName: "DNU Bad Name" } };
describe("MyComponent.vue", () => {
it.only("sanity test", async () => {
const wrapper = shallowMount(MyComponent);
expect(wrapper.vm.isEditable).toBe(true);
});
it.only("myData", async () => {
const wrapper = shallowMount(MyComponent);
Object.assign(wrapper.vm.myData, data);
expect(wrapper.vm.isEditable).toBe(false);
});
it.only("myData", async () => {
const wrapper = shallowMount(MyComponent);
wrapper.vm.myData.myObject = data.myObject;
expect(wrapper.vm.isEditable).toBe(false);
});
it.only("updateMyData method via return", async () => {
const wrapper = shallowMount(MyComponent);
wrapper.vm.updateMyData(data);
expect(wrapper.vm.isEditable).toBe(false);
});
it.only("updateMyData method via expose🙄", async () => {
const wrapper = shallowMount(MyComponent);
wrapper.__app._container._vnode.component.subTree.component.exposed.updateMyData(
data
);
expect(wrapper.vm.isEditable).toBe(false);
});
});
It is not possible through setData
from the docs:
setData
Updates component internal data.
Signature:
setData(data: Record<string, any>): Promise<void>
Details:
setData does not allow setting new properties that are not defined in the component.
Also, notice that setData does not modify composition API setup() data.
It seems that updating internals with composition API is incompatible with setData. See the method name setData, refers to this.data and was likely kept in the vue test utils mostly for backwards compatibility.
I suspect the theory is that it's bad practice anyway to test, what would be considered, an implementation detail and the component test should focus on validating inputs an outputs only. Fundamentally though, this is a technical issue, because the setup function doesn't expose the refs and reactives created in the setup.
There is a MUCH easier way to do this.....
Put your composables in a separate file
Test the composables stand alone.
Here is the vue file:
<template>
<div>
<div>value: {{ counter }}</div>
<div>isEven: {{ isEven }}</div>
<button type="button" #click="increment">Increment</button>
</div>
</template>
<script setup lang='ts'>
import {sampleComposable} from "./sample.composable";
const {isEven, counter, increment} = sampleComposable();
</script>
Here is the composable:
import {computed, ref} from 'vue';
export function sampleComputed() {
const counter = ref(0);
function increment() {
counter.value++;
}
const isEven = computed(() => counter.value % 2 === 0);
return {counter, increment, isEven};
}
Here is the test:
import {sampleComposable} from "./sample.composable";
describe('sample', () => {
it('simple', () => {
const computed = sampleComposable();
expect(computed.counter.value).toEqual(0);
expect(computed.isEven.value).toEqual(true);
computed.increment();
expect(computed.counter.value).toEqual(1);
expect(computed.isEven.value).toEqual(false);
computed.increment();
expect(computed.counter.value).toEqual(2);
expect(computed.isEven.value).toEqual(true);
})
});
This just 'works'. You don't have to deal w/ mounting components or any other stuff, you are JUST TESTING JAVASCRIPT. It's faster and much cleaner. It seems silly to test the template anyway.
One way to make this easier to test is to put all of your dependencies as arguments to the function. For instance, pass in the props so it's easy to just put in dummy values as need. Same for emits.
You can tests watches as well. You just need to flush the promise after setting the value that is being watched:
composable.someWatchedThing.value = 6.5;
await flushPromises();
Here is my flushPromises (which I found here):
export function flushPromises() {
return new Promise(process.nextTick);
}

Why Redux action is not Being being dispatched in Redux-Tooklit

I am using react-redux with redux and redux-toolkit. And according to this example, i created an async dispatch that calls the reducer action when resolved.
import { createSlice } from "#reduxjs/toolkit";
import axios from "axios";
export const BlogSlice = createSlice({
name: "Blog",
initialState: {
BlogList: null,
},
reducers: {
getBlogList: (state, action) => {
console.log(action.payload);
state.BlogList = action.payload;
}
},
});
export const { getBlogList } = BlogSlice.actions;
export const getBlogListAsync = (user_id) => (dispatch) => {
axios.get(`/api/blog/getblogs/${user_id}`).then((res) => {
console.log(res.data);
dispatch(getBlogList(res.data.result));
});
};
export const selectBlogList = (state) => state.Blog.BlogList;
export default BlogSlice.reducer;
I have used it in a component accordingly so that, the component dispatches getBlogListAsync and that logs the res.data but getBlogList is not being dispatched. I tried putting other console.log() but don't understand what is wrong.
A similar Slice is working perfectly with another Component.
It is hard to say for sure what's wrong here because there is nothing that is definitely wrong.
res.data.result?
You are logging res.data and then setting the blog list to res.data.result. My best guess as to your mistake is that res.data.result is not the the correct property for accessing the blogs, but I can't possibly know that without seeing your API.
console.log(res.data);
dispatch(getBlogList(res.data.result));
missing middleware?
Is there any chance that "thunk" middleware is not installed? If you are using Redux Toolkit and omitting the middleware entirely, then the thunk middleware will be installed by default. Also if this were the case you should be getting obvious errors, not just nothing happening.
it seems fine...
I tested out your code with a placeholder API and I was able to get it working properly. Maybe this code helps you identify the problem on your end. Code Sandbox Demo.
import React from "react";
import { createSlice, configureStore } from "#reduxjs/toolkit";
import axios from "axios";
import { Provider, useDispatch, useSelector } from "react-redux";
export const BlogSlice = createSlice({
name: "Blog",
initialState: {
BlogList: null
},
reducers: {
getBlogList: (state, action) => {
console.log(action.payload);
state.BlogList = action.payload;
}
}
});
export const { getBlogList } = BlogSlice.actions;
const store = configureStore({
reducer: {
Blog: BlogSlice.reducer
}
});
export const getBlogListAsync = (user_id) => (
dispatch: Dispatch
) => {
// your url `/api/blog/getblogs/${user_id}`
const url = `https://jsonplaceholder.typicode.com/posts?userId=${user_id}`; // placeholder URL
axios.get(url).then((res) => {
console.log(res.data);
// your list: res.data.result <-- double check this
const list = res.data; // placeholder list
dispatch(getBlogList(list));
});
};
export const selectBlogList = (state) => state.Blog.BlogList;
const Test = () => {
const dispatch = useDispatch();
const blogs = useSelector(selectBlogList);
const user_id = "1";
return (
<div>
<button onClick={() => dispatch(getBlogListAsync(user_id))}>
Load Blogs
</button>
<h3>Blog Data</h3>
<div>{JSON.stringify(blogs)}</div>
</div>
);
};
export default function App() {
return (
<Provider store={store}>
<Test />
</Provider>
);
}

Reference Error:Navigator not defined with nextjs

My code is like this:
import React, { useEffect } from 'react';
import alanBtn from '#alan-ai/alan-sdk-web';
const alanKey = my key;
const App = () => {
useEffect(() => {
alanBtn({
key: alanKey,
onCommand: ({ command }) => {
alert('This code was executed');
}
})
}, []);
return (
<div><h1>Alan AI News Application</h1></div>);
}
export default App;
But i am getting the error as:
Reference Error:Navigator not defined..
How to fix it?
Browser objects like window , navigator etc should be define in useEffect first before use.
const [pageURL, setPageURL] = useState("");
const [isNativeShare, setNativeShare] = useState(false);
useEffect(() => {
setPageURL(window.location.href);
if (navigator.share) {
setNativeShare(true);
}
}, []);
// Now, use can use pageURL , isNativeShare in code
This is not an issue with your Next.js code it's just the way you are supposed to call the alan-ai library.
Below is the solution that should work for you.
import React, { useEffect } from "react";
const alanKey = "my key";
function App() {
useEffect(() => {
const alanBtn = require("#alan-ai/alan-sdk-web");
alanBtn({
key: "myKey",
rootEl: document.getElementById("alan-btn")
});
}, []);
return (
<div>
<h1>Alan AI News Application</h1>
</div>
);
}
export default App;
Here is the discussion link for the same https://github.com/alan-ai/alan-sdk-web/issues/29#issuecomment-672242925.
Hope this solves your issue.
Happy Coding.

How can I implement in styled components and interpolations with emotion theming?

I have been working on a React web application with a dynamic theme using the emotion-theming library. So a user can switch between environments and each environment has its own theme.
I have created my own CustomThemeProvider which I use to dynamicly change the theme. Below is the code.
export interface CustomThemeContextValue {
customTheme?: Theme;
setCustomTheme: (theme: Theme) => void;
};
const CustomThemeContext = React.createContext<CustomThemeContextValue>({
customTheme: undefined,
setCustomTheme: (theme) => { }
});
interface CustomThemeProviderProps {
}
export const CustomThemeProvider: FC<CustomThemeProviderProps> = (props) => {
const [customTheme, setCustomTheme] = useState<Theme>(theme);
const context: CustomThemeContextValue = React.useMemo(() => ({
customTheme,
setCustomTheme
}), [customTheme, setCustomTheme]);
return (
<CustomThemeContext.Provider value={context}>
<ThemeProvider theme={customTheme} {...props} />
</CustomThemeContext.Provider>
);
};
export const useCustomTheme = () => {
const context = React.useContext(CustomThemeContext);
if (!context) {
throw new Error('useCustomTheme must be used within a CustomThemeProvider');
}
return context;
};
The provider is implemented in the root like so
const Root = () => {
return (
<StrictMode>
<CustomThemeProvider>
<Normalize />
<Global styles={globalStyle} />
<App />
</CustomThemeProvider>
</StrictMode>
);
};
So this code is working, I can get the theme within a function component using the emotion useTheme hook like below:
const theme: Theme = useTheme();
But the question is how to get the theme out of the emotion ThemeProvider and use it in certain situations. Is it possibe to use it in a context like
export const style: Interpolation = {
cssProp: value
};
Or is it usable in a context like below where styled.button is from emotion/styled.
const Button: FC<HTMLProps<HTMLButtonElement> & ButtonProps> = styled.button([]);
and is it usable in the emotion/core method css() like below
const style = css({
cssProp: value
});
I find it very hard to find answers to these questions using google so I hope somebody here can help me out.
So after a while I have finally found an answer to my own question and I like to share it with everyone as it is very hard to find. so here it is.
Instead of Interpolation you can use InterpolationWithTheme like below:
import { InterpolationWithTheme } from '#emotion/core';
export const style: InterpolationWithTheme<Theme> = (theme) => ({
cssProp: theme.value
});
This way you can get the theme out of the ThemeProvider.
With the styled componenents you can implement it like below:
const Button: FC<HTMLProps<HTMLButtonElement> & ButtonProps>
= styled.button(({ theme }: any) => ([
{
cssProp: theme.value
}
]);
And finally when you want to use the css() with the themeProvider you have to replace it with the InterpolationWithTheme to make it work just like in the first example of this answer.
These answers have been found by a combination of looking in the emotionjs docs and inspecting the emotionjs types/interfaces.

How can I get (query string) parameters from the URL in Next.js?

When I click on a link in my /index.js, it brings me to /about.js page.
However, when I'm passing parameter name through URL (like /about?name=leangchhean) from /index.js to /about.js, I don't know how to get it in the /about.js page.
index.js
import Link from 'next/link';
export default () => (
<div>
Click{' '}
<Link href={{ pathname: 'about', query: { name: 'leangchhean' } }}>
<a>here</a>
</Link>{' '}
to read more
</div>
);
Use router-hook.
You can use the useRouter hook in any component in your application.
https://nextjs.org/docs/api-reference/next/router#userouter
pass Param
import Link from "next/link";
<Link href={{ pathname: '/search', query: { keyword: 'this way' } }}><a>path</a></Link>
Or
import Router from 'next/router'
Router.push({
pathname: '/search',
query: { keyword: 'this way' },
})
In Component
import { useRouter } from 'next/router'
export default () => {
const router = useRouter()
console.log(router.query);
...
}
Using Next.js 9 or above you can get query parameters:
With router:
import { useRouter } from 'next/router'
const Index = () => {
const router = useRouter()
const {id} = router.query
return(<div>{id}</div>)
}
With getInitialProps:
const Index = ({id}) => {
return(<div>{id}</div>)
}
Index.getInitialProps = async ({ query }) => {
const {id} = query
return {id}
}
url prop is deprecated as of Next.js version 6:
https://github.com/zeit/next.js/blob/master/errors/url-deprecated.md
To get the query parameters, use getInitialProps:
For stateless components
import Link from 'next/link'
const About = ({query}) => (
<div>Click <Link href={{ pathname: 'about', query: { name: 'leangchhean' }}}><a>here</a></Link> to read more</div>
)
About.getInitialProps = ({query}) => {
return {query}
}
export default About;
For regular components
class About extends React.Component {
static getInitialProps({query}) {
return {query}
}
render() {
console.log(this.props.query) // The query is available in the props object
return <div>Click <Link href={{ pathname: 'about', query: { name: 'leangchhean' }}}><a>here</a></Link> to read more</div>
}
}
The query object will be like: url.com?a=1&b=2&c=3 becomes: {a:1, b:2, c:3}
For those looking for a solution that works with static exports, try the solution listed here: https://github.com/zeit/next.js/issues/4804#issuecomment-460754433
In a nutshell, router.query works only with SSR applications, but router.asPath still works.
So can either configure the query pre-export in next.config.js with exportPathMap (not dynamic):
return {
'/': { page: '/' },
'/about': { page: '/about', query: { title: 'about-us' } }
}
}
Or use router.asPath and parse the query yourself with a library like query-string:
import { withRouter } from "next/router";
import queryString from "query-string";
export const withPageRouter = Component => {
return withRouter(({ router, ...props }) => {
router.query = queryString.parse(router.asPath.split(/\?/)[1]);
return <Component {...props} router={router} />;
});
};
Get it by using the below code in the about.js page:
// pages/about.js
import Link from 'next/link'
export default ({ url: { query: { name } } }) => (
<p>Welcome to About! { name }</p>
)
I know 2 ways to do this:
A Server-Side way, and a Client-Side way.
Method #1: SSR (Server-Side Rendering):
You should use Query Context for that page.
So use getServerSideProps instead of getStaticProps
import React from "react";
export async function getServerSideProps(context) {
const page = (parseInt(context.query.page) || 1).toString();
// Here we got the "page" query parameter from Context
// Default value is "1"
const res = await fetch(`https://....com/api/products/?page=${page}`);
const products = await res.json();
return {props: {products: products.results}}
// will be passed to the page component as props
}
const Page = (props) =>{
const products = props.products;
return (
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>);
}
export default Page
The reason is that: this data cannot be pre-rendered ahead of user's request, so it must be Server-Side Rendered (SSR) on every request.
Static Pages: Use getStaticProps
Changing Content: use getServerSideProps
And here the content is changing based on query Parameters
Reference: https://nextjs.org/docs/api-reference/data-fetching/get-server-side-props
Method #2: Next Router (Client Side):
import {useState, useEffect} from "react";
import { useRouter } from 'next/router'
const Page = () =>{
const [products, setProducts] = useState([]);
const [page, setPage] =useState((useRouter().query.page || 1).toString());
// getting the page query parameter
// Default value is equal to "1"
useEffect(()=>{
(async()=>{
const res = await fetch(`https://....com/api/products/?page=${page}`);
const products = await res.json();
setProducts(products.results);
// This code will be executed only once at begining of the loading of the page
// It will not be executed again unless you cahnge the page
})()
},[page]);
return (
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}
export default Page
Reference: https://nextjs.org/docs/api-reference/next/router
If you need to retrieve a URL query from outside a component:
import router from 'next/router'
console.log(router.query)
import { useRouter } from 'next/router';
function componentName() {
const router = useRouter();
console.log('router obj', router);
}
We can find the query object inside a router using which we can get all query string parameters.
Using {useRouter} from "next/router"; helps but sometimes you won't get the values instead u get the param name itself as value.
This issue happens when u are trying to access query params via de-structuring like:
let { categoryId = "", sellerId = "" } = router.query;
and the solution that worked for me is try to access the value directly from query object:
let categoryId = router.query['categoryId'] || '';
let sellerId = router.query['sellerId'] || '';
Post.getInitialProps = async function(context) {
const data = {}
try{
data.queryParam = queryString.parse(context.req.url.split('?')[1]);
}catch(err){
data.queryParam = queryString.parse(window.location.search);
}
return { data };
};
import { useRouter } from 'next/router'
const Home = () => {
const router = useRouter();
const {param} = router.query
return(<div>{param}</div>)
}
Also you can use getInitialProps, more details refer the below tutorial.
get params from url in nextjs
What worked for me in Nextjs 13 pages in the app directory (SSR)
Pass params and searchParams to the page:
export default function SomePage(params, searchParams) {
console.log(params);
console.log(searchParams);
return <div>Hello, Next.js!</div>;
With some builds there may be a bug that can be solved by adding:
export const dynamic='force-dynamic';
especially when deploying on Vercel.
ref: https://beta.nextjs.org/docs/api-reference/file-conventions/page#searchparams-optional
https://github.com/vercel/next.js/issues/43077

Resources