Problem when dynamically registering routes in an application with microfrontends concept - reactjs

I have an Typescript + Redux (with RTK) application using the microfrontends concept. All the steps for the construction came from this tutorial: Microfrontends tutorial.
The main component is Microfrontend.tsx (omitted imports):
interface Manifest {
files: {
'main.js': string
'main.js.map': string
'index.html': string
}
entrypoints: string[]
}
const MicroFrontend = ({
name,
host,
module
}: {
name: string
host: string | undefined
module: string
}) => {
const history = useHistory()
useEffect(() => {
const renderMicroFrontend = () => {
// #ts-ignore
window[`render${name}`] && window[`render${name}`](`${name}-container`, history)
}
if (document.getElementById(name)) {
renderMicroFrontend()
return
}
const manifestUrl = `${
isDevProfile ? host : ''
}/${module}/view/asset-manifest.json`
fetch(manifestUrl)
.then(res => res.json())
.then((manifest: Manifest) => {
const script = document.createElement('script')
script.id = name
script.crossOrigin = ''
script.src = `${host}${manifest.files['main.js']}`
script.onload = () => {
renderMicroFrontend()
}
document.head.appendChild(script)
})
return () => {
// #ts-ignore
window[`unmount${name}`] && window[`unmount${name}`](`${name}-container`)
}
})
return (
<main id={`${name}-container`} style={{ height: '100%' }} />
)
}
MicroFrontend.defaultProps = {
document,
window
}
export default MicroFrontend
I'm trying to render the routes of the child components in a dynamic way, however, when I do this, I have a very strange effect: Bug.
The code snippet that generates this effect is this (omitted imports):
const App = () => {
const dispatch = useAppDispatch()
const { loadWithSuccess } = useSelector(moduleSelectors)
const avaibleModuleLinks = useSelector(avaibleModuleLinksWhitoutHome)
useEffect(() => {
dispatch(fetchAvaibleModules()).then(response =>
dispatch(fetchAvaibleModuleLinks(response.payload as string[]))
)
}, [dispatch])
return (
<BrowserRouter>
<Template>
<Switch>
<Route exact={true} path="/" component={Home} />
{loadWithSuccess ? avaibleModuleLinks?.map(
(subMenuPath: SubMenuPath | undefined, index: number) => {
const subMenuPathKey = subMenuPath ? subMenuPath.key : ''
let micro = () => (
<MicroFrontend
module={subMenuPathKey}
host="127.0.0.1"
name={subMenuPath ? subMenuPath.key.charAt(0).toUpperCase() : ''}
/>
)
return (
<Route
key={index}
path={`/dfe/view/${subMenuPathKey}`}
component={micro}
/>
)
}
): <></>}
</Switch>
</Template>
</BrowserRouter>
)
}
export default App
Only when I don't render routes dynamically do I have the desired effect: desired behavior
The code snippet that generates this effect is this (omitted imports):
const ModuleNfe = () => (
<MicroFrontend host="127.0.0.1" name="Nfe" module="nfe" />
)
const App = () => {
const dispatch = useAppDispatch()
const { loadWithSuccess } = useSelector(moduleSelectors)
const avaibleModuleLinks = useSelector(avaibleModuleLinksWhitoutHome)
useEffect(() => {
dispatch(fetchAvaibleModules()).then(response =>
dispatch(fetchAvaibleModuleLinks(response.payload as string[]))
)
}, [dispatch])
return (
<BrowserRouter>
<Template>
<Switch>
<Route exact={true} path="/" component={Home} />
<Route path="/dfe/view/nfe" component={ModuleNfe} />
</Switch>
</Template>
</BrowserRouter>
)
}
export default App
As you may have noticed, the desired behavior is for my page to be rendered inside the Template component. But for some reason, this is not the case.

Related

Why are there two different ways to use it?

the same component is stored in state and ref respectively, but the calling methods of the two are different.
export default function getAsyncComponent(load) {
const AsyncComponent = () => {
const [component, setComponent] = useState(null)
const com = useRef(null)
const loadComponent = async () => {
const _component = await load()
com.current = _component.default
setComponent(_component.default)
}
useEffect(() => {
loadComponent()
}, [])
console.log(component, com.current)
const Com = com.current
return (
<div>
// Both ways are works.
{component ? component : null}
{Com ? <Com /> : null}
</div>
)
}
return <AsyncComponent />
}
print:
{$$typeof: Symbol(react.element), type: 'div', key: null, ref: null, props: {…}, …}
and
ƒ IndexPage() {
return ((0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)("div", { children: [(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("img", { className: _index_less__WEBPACK_I…
The use of getAsyncComponent is like this.
function App() {
return (
<BrowserRouter>
<Routes>
<Route
path="/"
element={getAsyncComponent(() => import('src/pages/index'))}
/>
</Routes>
</BrowserRouter>
)
}
Are the above two calling methods different?
react#18.1
react-router-dom#6.3

Custom lazy import function is occured all file will be chunked under import path

In React Project, I was trying to dynamic lazy load with custom functions.
Why Custom function will be make all chunked files in container directory even if just declared not using it?
if I applied my custom lazyImport function in other code, results will be same. Doesn't care about the function is used that is enough just declared.
const lazyImport = (containerName: string) => lazy(() => import(`containers/${containerName}`));
const assignRouter: AssignRoute[] = Object.keys(routerMeta).map((componentKey: string) => {
const propsArr: propsArrTypes = assignRouteArrayProps(routerMeta[componentKey])
return {
Comp: lazyImport(componentKey),
propsArr
}
})
const assignRoute = (Comp: ComponentType<any>, props: RouteMetaProps) => {
return <Route key={props.path} element={<Comp />} {...props} />
}
const CommonRouter: FunctionComponent<ICustomRotuerProps> = (props) => {
return <Suspense fallback={<FlexCenter>
<Spin />
</FlexCenter>}>
<Routes>
{assignRouter.map(({ Comp, propsArr }: AssignRoute) => {
return assignRoute(Comp, (propsArr as RouteMetaType) as RouteMetaProps)
}
})}
</Routes>
</Suspense>;
};
export default CommonRouter

Make react-router-dom v6 pass path as key to rendered element

I think I may need a paradigm shift in my thinking here, so I'm open to those kinds of answers as well here.
Consider the following simplified example:
export const App = (p: { apiClient: SomeApiClient }) => {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/posts/:postId" element={<Post apiClient={p.apiClient} />} />
</Routes>
);
}
export const Home = () => {
return <h1>Home!</h1>
}
export const Post = (p: { apiClient: SomeApiClient }) => {
const { postId } = useParams();
const [ state, setState ] = useState<PostState>({ status: "loading" });
// When the component mounts, get the specified post from the API
useEffect(() => {
if (state.status === "loading") {
(async () => {
const post = await p.apiClient.getPost(postId);
setState({ status: "ready", post });
})();
}
})
return (
<h2>Posts</h2>
{
state.status === "loading"
? <p>Loading....</p>
: <div className="post">
<h3>{state.post.title}</h3>
<div className="content">{state.post.content}</div>
</div>
}
)
}
export type PostState =
| { status: "loading" }
| { status: "ready"; post: BlogPost };
export type BlogPost = { title: string; content: string };
This works fine the first time, but pretend there's a <Link /> on the page that goes to the next post. When I click that link, the URL changes, but the page content doesn't, because React Router is not actually re-mounting the <Post .../> component. That component correctly receives the updated postId and is re-rendered, but since it doesn't get re-mounted, the useEffect logic doesn't run again and the content stays the same.
I've been solving this very awkwardly by creating intermediary components like so:
export const App = (p: { apiClient: SomeApiClient }) => {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/posts/:postId" element={<PostRenderer apiClient={p.apiClient} />} />
</Routes>
);
}
export const PostRenderer = (p: { apiClient: SomeApiClient }) => {
const { postId } = useParams();
return <Post key={postId} postId={postId} apiClient={p.apiClient} />
}
export const Post = (p: { postId: string; apiClient: SomeApiClient }) => {
// ....
}
But I'm starting to get a lot of those, and literally all they do is take the param from the URL and use it as a key on the actual target component. I've read through the react-router-dom docs and am not finding anything that indicates there's a way to automate this. I must be thinking about this wrong.... Any suggestions are appreciated.
I think a more common and practical solution is to add the postId as a dependency to the useEffect to rerun the asynchronous logic when the route param changes.
Example:
export const Post = (p: { apiClient: SomeApiClient }) => {
const { postId } = useParams();
const [state, setState] = useState<PostState>({ status: "loading" });
// When the post id updates, get the specified post from the API
useEffect(() => {
const fetchPostById = async (postId) => {
setState({ status: "loading" });
const post = await p.apiClient.getPost(postId);
setState({ status: "ready", post });
};
fetchPostById(postId);
}, [postId]);
return (
<h2>Posts</h2>
{
state.status === "loading"
? <p>Loading....</p>
: <div className="post">
<h3>{state.post.title}</h3>
<div className="content">{state.post.content}</div>
</div>
}
)
};

React navigation createStackNavigator switch between routes

I'm using React native with Expo and i'm trying to make a dynamic redirect navigation in my component.
When the user come to my page I want to check an async value and with the result of this value I want the user to be redirected to one of the Screen.
I'm using AppLoading from expo and redux state. But it's seems that I can't use navigate at this moment on my Stack.
Here is my code :
const Stack = createStackNavigator();
export default function Mynavigation({ navigation }: BasicNavigationProps) {
const dispatch = useDispatch()
const { appLoading } = useSelector(userStateSelector)
const navigateToRoute = async () => {
const goToPage2 = await needToGoPage2()
if (goToPage2) {
navigation.navigate('Page2')
} else {
navigation.navigate('Page1')
}
}
return (
appLoading ? (
<Stack.Navigator initialRouteName="Page1" headerMode="none">
<Stack.Screen name="Page1" component={Page1}/>
<Stack.Screen name="Page2" component={Page2}/>
</Stack.Navigator>
) : (
<AppLoading
startAsync={ navigateToRoute }
onFinish={() => dispatch(saveAppLoading(false))}
onError={() => {
console.log('error')
}}
/>
)
)
}
Did I forgot something ?
I Implemented another solution with two different Stack navigation, but I don't know witch version is better to use ?
const Stack1 = createStackNavigator();
const Stack2 = createStackNavigator();
export default function Mynavigation({ navigation }: BasicNavigationProps) {
const dispatch = useDispatch()
const [check, setCheck] = useState(false)
const { appLoading } = useSelector(userStateSelector)
const navigateToRoute = async () => {
const goToPage2 = await needToGoPage2()
setCheck(goToPage2)
}
return (
appLoading ? (
check ? (
<Stack1.Navigator initialRouteName="Page1" headerMode="none">
<Stack1.Screen name="Page1" component={Page1Stack1}/>
<Stack1.Screen name="Page2" component={Page2Stack1}/>
</Stack1.Navigator>
) : (
<Stack2.Navigator initialRouteName="Page1" headerMode="none">
<Stack2.Screen name="Page1" component={Page1Stack2}/>
<Stack2.Screen name="Page2" component={Page2Stack2}/>
</Stack2.Navigator>
)
) : (
<AppLoading
startAsync={ navigateToRoute }
onFinish={() => dispatch(saveAppLoading(false))}
onError={() => {
console.log('error')
}}
/>
)
)
}
Thanks for your help
You are right. You can't use navigation props at this stage.
Remove it from the component props and try doing it like this:
const Stack = createStackNavigator();
export default function Mynavigation({ navigation }: BasicNavigationProps) {
const [initialRouteName, setInitialRouteName] = React.useState('');
const [loading, setLoading] = React.useState(true);
const navigateToRoute = async () => {
const goToPage2 = await needToGoPage2()
if (goToPage2) {
setInitialRouteName('Page2')
} else {
setInitialRouteName('Page1')
}
setLoading(false);
}
return (
loading === false && initialRouteName.length > 0 ? (
<Stack.Navigator initialRouteName={initialRouteName} headerMode="none">
<Stack.Screen name="Page1" component={Page1} />
<Stack.Screen name="Page2" component={Page2} />
</Stack.Navigator>
) : (
<AppLoading
startAsync={navigateToRoute}
onFinish={() => dispatch(saveAppLoading(false))}
onError={() => {
console.log('error')
}}
/>
)
)
}

Async throws error in React Functional Component

I am trying to use use async in following functional component but throws error
const RouteConfig = async ({ component: Component, fullLayout, user, auth, ...rest}) => (
<Route
{...rest}
render={props => {
return (
<ContextLayout.Consumer>
{context => {
let LayoutTag =
fullLayout === true
? context.fullLayout
: context.state.activeLayout === 'horizontal'
? context.horizontalLayout
: context.VerticalLayout
const verified = await verifyToken(auth.values)
return (auth.values !== undefined && auth.values.isSignedIn && verified) ? (
<LayoutTag {...props} permission='{user}'>
<Suspense fallback={<Spinner />}>
<Component {...props}></Component>
</Suspense>
</LayoutTag>
) : (
<context.fullLayout {...props} permission={user}>
<Suspense fallback={<Spinner />}>
<Login {...props} />
</Suspense>
</context.fullLayout>
)
}}
</ContextLayout.Consumer>
)
}}
/>
)
const mapStateToProps = state => {
return {
user: state.auth.login.userRole,
auth: state.auth.login
}
}
const AppRoute = connect(mapStateToProps)(RouteConfig)
below is the verifyToken Function which return true or false
const verifyToken = async props => {
if (props.accessToken !== undefined) {
//if (props !== undefined) {
if (assertAlive(jwt.decode(props.accessToken))) {
const verified = await refreshToken(props)
console.log(verified)
if (verified){
console.log('Authorized')
return true
} else {
console.log('Unauthorized')
return false
}
} else {
return false
}
}else
return false
}
function assertAlive (decoded) {
const now = Date.now().valueOf() / 1000
if (typeof decoded.exp !== 'undefined' && decoded.exp < now) {
//throw new Error(`token expired: ${JSON.stringify(decoded)}`)
return false
}
if (typeof decoded.nbf !== 'undefined' && decoded.nbf > now) {
//throw new Error(`token not yet valid: ${JSON.stringify(decoded)}`)
return false
}
return true
}
above used refreshToken has a functional which gets response from an API call
export const refreshToken = async () => {
const options = { withCredentials: true };
const resp = await axios.post('http://localhost:4000/api/auth/verifyToken',{}, options).catch(err => {console.log(err); return false;});
//console.log(resp.status)
if (resp.status === 200){
//console.log('200 passed')
return true
}else if (resp.status === 401){
//console.log('401 failed')
return false
}else{
//console.log('204 failed')
return false
}
}
Any suggestions would be grateful to fix this issue. If someone has better way to code this please let me know. I am bit new into ReactJS and open for suggestions. FIrst function is used to check on each route if Token is authorized or not. If its is authorized it allows to access page and if not redirects to Login Page.
After suggestions from #Mordechai, I have made below changes but it throws an error
./src/Router.js
Line 193:40: React Hook "useState" is called in function "verified" which is neither a React function component or a custom React Hook function react-hooks/rules-of-hooks
Line 194:3: React Hook "useEffect" is called in function "verified" which is neither a React function component or a custom React Hook function react-hooks/rules-of-hooks
function verified(auth){
const [verified, setVerifiedValue] = useState(verifyToken(auth.values));
useEffect(() => { setVerifiedValue(verifyToken(auth.values) )})
return verified;
}
const RouteConfig = ({ component: Component, fullLayout, user, auth, ...rest}) => (
<Route
{...rest}
render={props => {
//useEffect(() => {const verified = verifyToken(auth.values) });
return (
<ContextLayout.Consumer>
{context => {
let LayoutTag =
fullLayout === true
? context.fullLayout
: context.state.activeLayout === 'horizontal'
? context.horizontalLayout
: context.VerticalLayout
console.log(VerifiedToken)
return (auth.values !== undefined && auth.values.isSignedIn && VerifiedToken) ? (
<LayoutTag {...props} permission='{user}'>
<Suspense fallback={<Spinner />}>
<Component {...props}></Component>
</Suspense>
</LayoutTag>
) : (
<context.fullLayout {...props} permission={user}>
<Suspense fallback={<Spinner />}>
<Login {...props} />
</Suspense>
</context.fullLayout>
)
}}
</ContextLayout.Consumer>
)
}}
/>
)
const mapStateToProps = state => {
return {
user: state.auth.login.userRole,
auth: state.auth.login
}
}
const AppRoute = connect(mapStateToProps)(RouteConfig)
const VerifiedToken = connect(mapStateToProps)(verified)
Hooks must be named useXxx otherwise eslint will complain. Also, hooks must be called on the top-level of a function component.
You call verifyToken() in both the useState() default value param and in the effect. If it's a long process you should only do it in the effect.
If you want to call verifyToken() in useVerify() just once in the lifetime of the component, you should add an empty array in the useEffect() dependency array
function useVerified(auth){
const [verified, setVerifiedValue] = useState();
useEffect(() => {
const doVerify = async () => {
setVerifiedValue(await verifyToken(auth.values))
}
doVerify()
}, [])
return verified;
}
const RouteConfig = ({ component: Component, fullLayout, user, auth, ...rest}) => {
const verified = useVerified(auth);
return <Route
{...rest}
render={props => {
return (
<ContextLayout.Consumer>
{context => {
let LayoutTag =
fullLayout === true
? context.fullLayout
: context.state.activeLayout === 'horizontal'
? context.horizontalLayout
: context.VerticalLayout
console.log(VerifiedToken)
return (auth.values !== undefined && auth.values.isSignedIn && VerifiedToken) ? (
<LayoutTag {...props} permission='{user}'>
<Suspense fallback={<Spinner />}>
<Component {...props}></Component>
</Suspense>
</LayoutTag>
) : (
<context.fullLayout {...props} permission={user}>
<Suspense fallback={<Spinner />}>
<Login {...props} />
</Suspense>
</context.fullLayout>
)
}}
</ContextLayout.Consumer>
)
}}
/>
}
const mapStateToProps = state => {
return {
user: state.auth.login.userRole,
auth: state.auth.login
}
}
const AppRoute = connect(mapStateToProps)(RouteConfig)
const VerifiedToken = connect(mapStateToProps)(verified)
Does the following work?
const NONE = {};
const RouteConfig = ({
component: Component,
fullLayout,
user,
auth,
...rest
}) => (
<Route
{...rest}
render={(props) => {
const [verified, setVerified] = React.useState(NONE);
React.useEffect(
() => verifyToken(auth.values).then(setVerified),
[]
);
if (verified === NONE) {
return null;
}
return (
<ContextLayout.Consumer>
{(context) => {
let LayoutTag =
fullLayout === true
? context.fullLayout
: context.state.activeLayout ===
'horizontal'
? context.horizontalLayout
: context.VerticalLayout;
return auth.values !== undefined &&
auth.values.isSignedIn &&
verified ? (
<LayoutTag {...props} permission="{user}">
<Suspense fallback={<Spinner />}>
<Component {...props}></Component>
</Suspense>
</LayoutTag>
) : (
<context.fullLayout
{...props}
permission={user}
>
<Suspense fallback={<Spinner />}>
<Login {...props} />
</Suspense>
</context.fullLayout>
);
}}
</ContextLayout.Consumer>
);
}}
/>
);

Resources