Vue.js 3 runtime core library providing foundational APIs for building custom renderers and managing reactive component systems
—
Vue's Server-Side Rendering (SSR) context system enables sharing data between server and client during the hydration process, supporting universal applications with server-rendered initial state.
Access and provide SSR context data during server-side rendering.
/**
* Gets the current SSR context (only available during SSR)
* @returns SSR context object or undefined on client
*/
function useSSRContext<T = Record<string, any>>(): T | undefined;
/**
* Injection key for SSR context
*/
const ssrContextKey: InjectionKey<Record<string, any>>;Usage Examples:
import { defineComponent, useSSRContext, ssrContextKey, inject } from "@vue/runtime-core";
// Basic SSR context usage
const SSRComponent = defineComponent({
setup() {
const ssrContext = useSSRContext();
if (ssrContext) {
// We're on the server
console.log('Rendering on server');
// Add data to SSR context
ssrContext.title = 'My App - Home Page';
ssrContext.meta = {
description: 'Welcome to my application',
keywords: 'vue, ssr, typescript'
};
// Add CSS files to be included
ssrContext.css = ssrContext.css || [];
ssrContext.css.push('/styles/home.css');
// Add preload hints
ssrContext.preloadLinks = ssrContext.preloadLinks || [];
ssrContext.preloadLinks.push({
href: '/api/data',
as: 'fetch',
crossorigin: 'anonymous'
});
} else {
// We're on the client
console.log('Hydrating on client');
}
return {};
}
});
// Using injection key for SSR context
const InjectionComponent = defineComponent({
setup() {
const ssrContext = inject(ssrContextKey);
if (ssrContext) {
// Server-side logic
ssrContext.userData = {
timestamp: Date.now(),
userAgent: 'server'
};
}
return {};
}
});
// Conditional rendering based on SSR
const UniversalComponent = defineComponent({
setup() {
const isSSR = !!useSSRContext();
return () => {
if (isSSR) {
return h('div', 'Server-rendered content');
} else {
return h('div', 'Client-rendered content');
}
};
}
});Manage data transfer between server and client rendering.
// SSR data store
const useSSRDataStore = () => {
const ssrContext = useSSRContext();
const setSSRData = (key: string, data: any) => {
if (ssrContext) {
ssrContext.state = ssrContext.state || {};
ssrContext.state[key] = data;
}
};
const getSSRData = (key: string, defaultValue: any = null) => {
if (ssrContext?.state) {
return ssrContext.state[key] ?? defaultValue;
}
// On client, try to get from window.__SSR_STATE__
if (typeof window !== 'undefined' && window.__SSR_STATE__) {
return window.__SSR_STATE__[key] ?? defaultValue;
}
return defaultValue;
};
return {
setSSRData,
getSSRData,
isSSR: !!ssrContext
};
};
// Usage in components
const DataComponent = defineComponent({
async setup() {
const { setSSRData, getSSRData, isSSR } = useSSRDataStore();
const data = ref(null);
if (isSSR) {
// Fetch data on server
try {
const response = await fetch('https://api.example.com/data');
const serverData = await response.json();
data.value = serverData;
setSSRData('componentData', serverData);
} catch (error) {
console.error('SSR data fetch failed:', error);
setSSRData('componentData', null);
}
} else {
// Get data from SSR state on client
data.value = getSSRData('componentData');
// If no SSR data, fetch on client
if (!data.value) {
const response = await fetch('https://api.example.com/data');
data.value = await response.json();
}
}
return { data };
}
});Manage document metadata during server-side rendering.
// Head management composable
const useSSRHead = () => {
const ssrContext = useSSRContext();
const setTitle = (title: string) => {
if (ssrContext) {
ssrContext.title = title;
} else if (typeof document !== 'undefined') {
document.title = title;
}
};
const addMeta = (meta: { name?: string; property?: string; content: string }) => {
if (ssrContext) {
ssrContext.meta = ssrContext.meta || [];
ssrContext.meta.push(meta);
} else if (typeof document !== 'undefined') {
const metaElement = document.createElement('meta');
if (meta.name) metaElement.name = meta.name;
if (meta.property) metaElement.setAttribute('property', meta.property);
metaElement.content = meta.content;
document.head.appendChild(metaElement);
}
};
const addLink = (link: { rel: string; href: string; [key: string]: string }) => {
if (ssrContext) {
ssrContext.links = ssrContext.links || [];
ssrContext.links.push(link);
} else if (typeof document !== 'undefined') {
const linkElement = document.createElement('link');
Object.entries(link).forEach(([key, value]) => {
linkElement.setAttribute(key, value);
});
document.head.appendChild(linkElement);
}
};
const addScript = (script: { src?: string; innerHTML?: string; [key: string]: any }) => {
if (ssrContext) {
ssrContext.scripts = ssrContext.scripts || [];
ssrContext.scripts.push(script);
} else if (typeof document !== 'undefined') {
const scriptElement = document.createElement('script');
if (script.src) scriptElement.src = script.src;
if (script.innerHTML) scriptElement.innerHTML = script.innerHTML;
Object.entries(script).forEach(([key, value]) => {
if (key !== 'src' && key !== 'innerHTML') {
scriptElement.setAttribute(key, value);
}
});
document.head.appendChild(scriptElement);
}
};
return {
setTitle,
addMeta,
addLink,
addScript
};
};
// SEO component
const SEOComponent = defineComponent({
props: {
title: String,
description: String,
keywords: String,
image: String,
url: String
},
setup(props) {
const { setTitle, addMeta, addLink } = useSSRHead();
// Set page title
if (props.title) {
setTitle(props.title);
}
// Basic meta tags
if (props.description) {
addMeta({ name: 'description', content: props.description });
}
if (props.keywords) {
addMeta({ name: 'keywords', content: props.keywords });
}
// Open Graph meta tags
if (props.title) {
addMeta({ property: 'og:title', content: props.title });
}
if (props.description) {
addMeta({ property: 'og:description', content: props.description });
}
if (props.image) {
addMeta({ property: 'og:image', content: props.image });
}
if (props.url) {
addMeta({ property: 'og:url', content: props.url });
addLink({ rel: 'canonical', href: props.url });
}
// Twitter Card meta tags
addMeta({ name: 'twitter:card', content: 'summary_large_image' });
if (props.title) {
addMeta({ name: 'twitter:title', content: props.title });
}
if (props.description) {
addMeta({ name: 'twitter:description', content: props.description });
}
if (props.image) {
addMeta({ name: 'twitter:image', content: props.image });
}
return () => null; // This component doesn't render anything
}
});Optimize performance with resource hints and preloading.
// Resource preloading
const useSSRPreload = () => {
const ssrContext = useSSRContext();
const preloadResource = (href: string, as: string, crossorigin?: string) => {
if (ssrContext) {
ssrContext.preloadLinks = ssrContext.preloadLinks || [];
ssrContext.preloadLinks.push({
rel: 'preload',
href,
as,
...(crossorigin && { crossorigin })
});
} else if (typeof document !== 'undefined') {
const link = document.createElement('link');
link.rel = 'preload';
link.href = href;
link.as = as;
if (crossorigin) link.crossOrigin = crossorigin;
document.head.appendChild(link);
}
};
const prefetchResource = (href: string) => {
if (ssrContext) {
ssrContext.prefetchLinks = ssrContext.prefetchLinks || [];
ssrContext.prefetchLinks.push({
rel: 'prefetch',
href
});
} else if (typeof document !== 'undefined') {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = href;
document.head.appendChild(link);
}
};
return {
preloadResource,
prefetchResource
};
};
// Critical CSS management
const useSSRCriticalCSS = () => {
const ssrContext = useSSRContext();
const addCriticalCSS = (css: string) => {
if (ssrContext) {
ssrContext.criticalCSS = ssrContext.criticalCSS || [];
ssrContext.criticalCSS.push(css);
}
};
const addStylesheet = (href: string, media = 'all') => {
if (ssrContext) {
ssrContext.stylesheets = ssrContext.stylesheets || [];
ssrContext.stylesheets.push({ href, media });
} else if (typeof document !== 'undefined') {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = href;
link.media = media;
document.head.appendChild(link);
}
};
return {
addCriticalCSS,
addStylesheet
};
};// SSR-safe component wrapper
const createSSRSafeComponent = <P extends Record<string, any>>(
component: Component<P>,
fallback?: Component<P>
) => {
return defineComponent({
props: component.props,
setup(props, ctx) {
const isSSR = !!useSSRContext();
if (isSSR && fallback) {
return () => h(fallback, props);
}
return () => h(component, props);
}
});
};
// Client-only component
const ClientOnly = defineComponent({
setup(_, { slots }) {
const isSSR = !!useSSRContext();
const isMounted = ref(false);
onMounted(() => {
isMounted.value = true;
});
return () => {
if (isSSR || !isMounted.value) {
return slots.fallback?.() || null;
}
return slots.default?.();
};
}
});
// SSR context provider
const SSRContextProvider = defineComponent({
props: {
context: { type: Object, required: true }
},
setup(props, { slots }) {
provide(ssrContextKey, props.context);
return () => slots.default?.();
}
});
// Universal data fetching
const useUniversalFetch = <T>(
url: string,
options: RequestInit = {}
): { data: Ref<T | null>; loading: Ref<boolean>; error: Ref<Error | null> } => {
const data = ref<T | null>(null);
const loading = ref(true);
const error = ref<Error | null>(null);
const { setSSRData, getSSRData, isSSR } = useSSRDataStore();
const cacheKey = `fetch_${url}`;
const fetchData = async () => {
try {
loading.value = true;
error.value = null;
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
data.value = result;
if (isSSR) {
setSSRData(cacheKey, result);
}
} catch (err) {
error.value = err instanceof Error ? err : new Error(String(err));
if (isSSR) {
setSSRData(cacheKey, null);
}
} finally {
loading.value = false;
}
};
if (isSSR) {
// Fetch on server
fetchData();
} else {
// Try to get from SSR cache first
const cachedData = getSSRData(cacheKey);
if (cachedData !== null) {
data.value = cachedData;
loading.value = false;
} else {
// Fetch on client if not in cache
fetchData();
}
}
return { data, loading, error };
};interface SSRContext extends Record<string, any> {
// HTML document metadata
title?: string;
meta?: Array<{ name?: string; property?: string; content: string }>;
links?: Array<Record<string, string>>;
scripts?: Array<{ src?: string; innerHTML?: string; [key: string]: any }>;
// Resource optimization
preloadLinks?: Array<{ rel: 'preload'; href: string; as: string; crossorigin?: string }>;
prefetchLinks?: Array<{ rel: 'prefetch'; href: string }>;
stylesheets?: Array<{ href: string; media?: string }>;
criticalCSS?: string[];
// Application state
state?: Record<string, any>;
// Request context (server-only)
url?: string;
request?: any;
response?: any;
}
// Global SSR state interface
declare global {
interface Window {
__SSR_STATE__?: Record<string, any>;
}
}
type SSRContextKey = InjectionKey<SSRContext>;// Server-side rendering with Express
app.get('*', async (req, res) => {
const ssrContext: SSRContext = {
url: req.url,
request: req,
response: res,
state: {}
};
try {
const html = await renderToString(app, ssrContext);
// Build HTML with SSR context data
const finalHtml = `
<!DOCTYPE html>
<html>
<head>
<title>${ssrContext.title || 'My App'}</title>
${ssrContext.meta?.map(meta =>
`<meta ${meta.name ? `name="${meta.name}"` : ''}
${meta.property ? `property="${meta.property}"` : ''}
content="${meta.content}">`
).join('\n') || ''}
${ssrContext.preloadLinks?.map(link =>
`<link rel="${link.rel}" href="${link.href}" as="${link.as}"
${link.crossorigin ? `crossorigin="${link.crossorigin}"` : ''}>`
).join('\n') || ''}
${ssrContext.criticalCSS?.map(css =>
`<style>${css}</style>`
).join('\n') || ''}
</head>
<body>
<div id="app">${html}</div>
<script>
window.__SSR_STATE__ = ${JSON.stringify(ssrContext.state)};
</script>
<script src="/dist/client.js"></script>
</body>
</html>
`;
res.send(finalHtml);
} catch (error) {
console.error('SSR Error:', error);
res.status(500).send('Server Error');
}
});Install with Tessl CLI
npx tessl i tessl/npm-vue--runtime-core