Implement a Custom Router View Component in Vue.js with TypeScript
This challenge asks you to build a fundamental part of a routing system in a Vue.js application. You will create a reusable component that acts as a placeholder for dynamically rendered content based on the current route. This is crucial for building single-page applications (SPAs) where different views or pages are loaded without full page reloads.
Problem Description
Your task is to create a TypeScript-based Vue.js component named RouterView. This component should:
- Display the current route's component: It needs to dynamically render the component associated with the current route.
- Respond to route changes: When the route changes, the
RouterViewcomponent should update to display the new route's component. - Handle nested routes (optional but recommended): For a more robust solution, consider how your
RouterViewwould handle nested routing, typically by rendering anotherRouterViewwithin the nested route's component.
Key Requirements:
- The
RouterViewcomponent should be a functional component or a regular component that accepts no props. - It needs to access the current route information (e.g., the component to render) from a hypothetical routing system. For this challenge, you can assume a global
routerobject or a reactive store that holds the current route. - The rendered component should be mounted and unmounted correctly as the route changes.
Expected Behavior:
When a route changes, the RouterView should:
- Unmount the currently rendered component.
- Mount the component corresponding to the new route.
- If the new component has specific props or data it needs from the route, these should be passed correctly. (For simplicity in this challenge, assume the component itself handles its data based on route parameters, or no route-specific data is needed).
Edge Cases to Consider:
- Initial Load: What component is rendered when the application first loads and a route is matched?
- No Matching Route: What happens if no route matches the current URL? (For this challenge, you can choose to render nothing or a default "not found" component).
- Component Lifecycle: Ensure component lifecycle hooks (like
mounted,unmounted) are called correctly for the dynamically rendered components.
Examples
For this challenge, we'll simulate a basic routing system and its interaction with RouterView.
Simulated Routing System:
Imagine a simple reactive system that emits route changes.
// Assume this is provided by a routing library or your own implementation
interface RouteRecord {
path: string;
component: any; // Vue component definition
children?: RouteRecord[];
}
interface CurrentRoute {
path: string;
component: any; // The component to render
params: Record<string, string>;
}
class MockRouter {
private routes: RouteRecord[];
private _currentRoute: CurrentRoute | null = null;
constructor(routes: RouteRecord[]) {
this.routes = routes;
// Initialize with the initial route
this.updateRoute(window.location.pathname || '/');
}
get currentRoute(): CurrentRoute | null {
return this._currentRoute;
}
// Simulates a route change
navigate(path: string): void {
this.updateRoute(path);
}
private updateRoute(path: string): void {
// Basic route matching logic (can be more complex)
const matchedRoute = this.matchRoute(this.routes, path);
if (matchedRoute) {
this._currentRoute = {
path: matchedRoute.path,
component: matchedRoute.component,
params: {}, // Simplified: no param extraction for this example
};
// In a real app, this would trigger a re-render
console.log(`Route changed to: ${path}, Component: ${this._currentRoute.component.__name || 'Anonymous'}`);
} else {
this._currentRoute = null;
console.log(`Route changed to: ${path}, No matching route.`);
}
// This would typically be handled by a reactive observer
// For this exercise, assume the RouterView "knows" about route changes
}
private matchRoute(routes: RouteRecord[], path: string): RouteRecord | undefined {
for (const route of routes) {
if (route.path === path) {
return route;
}
if (route.children) {
const matchedChild = this.matchRoute(route.children, path);
if (matchedChild) {
return matchedChild;
}
}
}
return undefined;
}
}
// Example Usage:
// const router = new MockRouter([...]);
// router.navigate('/about');
Example 1: Basic Route Rendering
Input:
- A
RouterViewcomponent. - A mock router configured with two routes:
/home->HomeComponent/about->AboutComponent
- The current route is
/home.
Expected Output (Vue Component Structure):
The RouterView should render the HomeComponent.
Explanation:
The RouterView checks the currentRoute from the mock router. It finds that the current route's component is HomeComponent and dynamically renders it within its own template.
Example 2: Route Change
Input:
- The same setup as Example 1.
- The current route is initially
/home, renderingHomeComponent. - The router navigates to
/about.
Expected Output (Vue Component Structure):
The RouterView should first unmount HomeComponent and then render AboutComponent.
Explanation:
The RouterView detects the route change. It calls the unmount lifecycle hook on HomeComponent, then retrieves the new component (AboutComponent) from the router and mounts it.
Example 3: Nested Routes (Conceptual)
Input:
- A
RouterViewcomponent. - A mock router configured with nested routes:
/userschildren:/users/list->UserListComponent/users/:id->UserProfileComponent
- The current route is
/users/list.
Expected Output (Vue Component Structure):
The main RouterView renders a UsersLayoutComponent (or similar). UsersLayoutComponent itself contains another <router-view> which then renders UserListComponent.
Explanation:
This demonstrates how nesting works. The top-level RouterView renders a component that is responsible for a layout. That layout component then contains its own <router-view> to handle rendering specific child views within its context. For this challenge, you can focus on the primary RouterView's logic.
Constraints
- The implementation of
RouterViewmust be in TypeScript. - You should leverage Vue 3's Composition API or Options API. Composition API is generally preferred for more dynamic component logic.
- Assume the existence of a mechanism to access the current route's component (e.g., a global reactive object, a provide/inject pattern, or a simple global variable for this exercise). You do not need to implement the entire routing matching logic or history management yourself. Focus on the
RouterViewcomponent. - The dynamically rendered components should be treated as opaque VNodes or Vue component definitions that the
RouterViewcan mount. - Performance is important; avoid unnecessary re-renders of the
RouterViewitself if the route component hasn't changed.
Notes
- Think about how Vue.js handles dynamic component rendering. The
<component :is="currentComponent">syntax in Vue templates is a key tool here. - Consider how to access the route information. If using Vue 3,
provideandinjectcan be a good pattern for passing down router instances or reactive route objects. For simplicity in this exercise, you might access a globally available reactive route object directly withinRouterView's setup. - The core of this challenge is managing the lifecycle of dynamically mounted components.
- You will likely need to use Vue's built-in
defineComponentorreactiveAPIs. - To simulate route changes for testing your
RouterView, you can create a simple mock function that updates a reactive variable representing thecurrentRoute.