Skip to content

Commit

Permalink
feat: refactor React Server Components support
Browse files Browse the repository at this point in the history
  • Loading branch information
edodusi committed Oct 18, 2024
1 parent ecaf457 commit b6293aa
Show file tree
Hide file tree
Showing 22 changed files with 553 additions and 173 deletions.
2 changes: 2 additions & 0 deletions lib/common/client.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import { useEffect, useState } from 'react';
import type { TUseStoryblokState } from '../types';
import { registerStoryblokBridge } from '@storyblok/js';
Expand Down
3 changes: 2 additions & 1 deletion lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@
"react-dom": "^17.0.0 || ^18.0.0"
},
"dependencies": {
"@storyblok/js": "^3.1.1"
"@storyblok/js": "^3.1.1",
"next": "14.2.14"
},
"devDependencies": {
"@babel/core": "^7.25.2",
Expand Down
46 changes: 26 additions & 20 deletions lib/rsc/common.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,76 @@
import { storyblokInit as sbInit } from "@storyblok/js";

import {
import { storyblokInit as sbInit } from '@storyblok/js';
import type {
ISbStoryData,
SbReactComponentsMap,
SbReactSDKOptions,
StoryblokClient,
} from "../types";
} from '../types';

let storyblokApiInstance: StoryblokClient = null;
let componentsMap: SbReactComponentsMap = {};
const componentsMap: Map<string, React.ElementType> = new Map<string, React.ElementType>();
let enableFallbackComponent: boolean = false;
let customFallbackComponent: React.ElementType = null;

// Cache for storyblok stories to allow for live editing
globalThis.storyCache = new Map<string, ISbStoryData>();

export const useStoryblokApi = (): StoryblokClient => {
if (!storyblokApiInstance) {
console.error(
"You can't use getStoryblokApi if you're not loading apiPlugin."
'You can\'t use getStoryblokApi if you\'re not loading apiPlugin.',
);
}

return storyblokApiInstance;
};

export const setComponents = (newComponentsMap: SbReactComponentsMap) => {
componentsMap = newComponentsMap;
Object.entries(newComponentsMap).forEach(([key, value]) => {
componentsMap.set(key, value);
});
return componentsMap;
};

export const getComponent = (componentKey: string) => {
if (!componentsMap[componentKey]) {
export const getComponent = (componentKey: string): React.ElementType | false => {
if (!componentsMap.has(componentKey)) {
console.error(`Component ${componentKey} doesn't exist.`);
return false;
}

return componentsMap[componentKey];
return componentsMap.get(componentKey);
};

export const getEnableFallbackComponent = () => enableFallbackComponent;
export const getCustomFallbackComponent = () => customFallbackComponent;

export const storyblokInit = (
pluginOptions: SbReactSDKOptions = {}
): (() => StoryblokClient) => {
export const storyblokInit = (pluginOptions: SbReactSDKOptions = {},): (() => StoryblokClient) => {
if (storyblokApiInstance) {
return () => storyblokApiInstance;
}

const { storyblokApi } = sbInit(pluginOptions);
storyblokApiInstance = storyblokApi;

componentsMap = pluginOptions.components;
if (pluginOptions.components) {
setComponents(pluginOptions.components);
}
enableFallbackComponent = pluginOptions.enableFallbackComponent;
customFallbackComponent = pluginOptions.customFallbackComponent;

return () => storyblokApi;
};

export { default as StoryblokComponent } from "./storyblok-component";
export * from '../types';
export { useStoryblokApi as getStoryblokApi };
export { default as SbServerComponent } from './storyblok-server-component';

export {
storyblokEditable,
apiPlugin,
loadStoryblokBridge,
useStoryblokBridge,
registerStoryblokBridge,
renderRichText,
RichTextResolver,
RichTextSchema,
} from "@storyblok/js";

export * from "../types";
storyblokEditable,
useStoryblokBridge,
} from '@storyblok/js';
8 changes: 4 additions & 4 deletions lib/rsc/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from "./common";
export { default as StoryblokStory } from "./story";
export { default as BridgeLoader } from "../bridge-loader";
export { default as StoryblokBridgeLoader } from "../bridge-loader";
export { default as BridgeLoader } from '../bridge-loader';
export { default as StoryblokBridgeLoader } from '../bridge-loader';
export * from './common';
export { default as SbStoryServerComponent } from './story-server-component';
19 changes: 19 additions & 0 deletions lib/rsc/live-edit-update-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use server';

export async function liveEditUpdateAction({ story, pathToRevalidate }) {
if (!story || !pathToRevalidate) {
console.error('liveEditUpdateAction: story or pathToRevalidate is not provided');
return;
}

globalThis.storyCache.set(story.uuid, story);

if (process.env.NEXT_RUNTIME) {
import('next/cache').then(({ revalidatePath }) => {
console.log('Revalidating path:', pathToRevalidate);
revalidatePath(pathToRevalidate);
}).catch((error) => {
console.error('liveEditUpdateAction: error while revalidating path', error);
});
}
}
39 changes: 39 additions & 0 deletions lib/rsc/story-server-component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, { forwardRef } from 'react';
import type { ISbStoryData, StoryblokBridgeConfigV2 } from '../types';
import { SbServerComponent } from './common';
import StoryblokLiveEditing from './storyblok-live-editing';

interface SbStoryServerComponentProps {
story: ISbStoryData;
bridgeOptions: StoryblokBridgeConfigV2;
[key: string]: unknown;
}

const SbStoryServerComponent = forwardRef<HTMLElement, SbStoryServerComponentProps>(
({ story, bridgeOptions, ...restProps }, ref) => {
if (!story) {
console.error(
'Please provide a \'story\' property to the SbStoryServerComponent',
);
return null;
}

if (!globalThis.storyCache.has(story.uuid)) {
globalThis.storyCache.set(story.uuid, story);
} else {
story = globalThis.storyCache.get(story.uuid);
}
if (typeof story.content === 'string') {
story.content = JSON.parse(story.content);
}

return (
<>
<SbServerComponent ref={ref} blok={story.content} {...restProps} />
<StoryblokLiveEditing story={story} bridgeOptions={bridgeOptions} />
</>
);
},
);

export default SbStoryServerComponent;
24 changes: 0 additions & 24 deletions lib/rsc/story.tsx

This file was deleted.

29 changes: 29 additions & 0 deletions lib/rsc/storyblok-live-editing.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"use client";

import { registerStoryblokBridge } from "@storyblok/js";
import { useEffect, startTransition } from "react";
import { liveEditUpdateAction } from "./live-edit-update-action";

const StoryblokLiveEditing = ({ story = null, bridgeOptions = {} }) => {
if (typeof window === 'undefined') {
return null;
}

const handleInput = (story) => {
if (!story) {
return;
}
startTransition(() => {
liveEditUpdateAction({ story, pathToRevalidate: window.location.pathname });
});
};

const storyId = story?.internalId ?? story?.id ?? 0;
useEffect(() => {
registerStoryblokBridge(storyId, (newStory) => handleInput(newStory), bridgeOptions);
}, []);

return null;
};

export default StoryblokLiveEditing;
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import React, { forwardRef } from "react";
import React, { forwardRef } from 'react';
import {
getComponent,
getEnableFallbackComponent,
getCustomFallbackComponent,
} from "./index";
import type { SbBlokData } from "../types";
getEnableFallbackComponent,
} from './index';
import type { SbBlokData } from '../types';

interface StoryblokComponentProps {
interface SbServerComponentProps {
blok: SbBlokData;
[key: string]: unknown;
}

const StoryblokComponent = forwardRef<HTMLElement, StoryblokComponentProps>(
const SbServerComponent = forwardRef<HTMLElement, SbServerComponentProps>(
({ blok, ...restProps }, ref) => {
if (!blok) {
console.error(
"Please provide a 'blok' property to the StoryblokComponent"
'Please provide a \'blok\' property to the StoryblokComponent',
);
return (
<div>Please provide a blok property to the StoryblokComponent</div>
Expand All @@ -33,22 +33,25 @@ const StoryblokComponent = forwardRef<HTMLElement, StoryblokComponentProps>(

if (CustomFallbackComponent) {
return <CustomFallbackComponent blok={blok} {...restProps} />;
} else {
}
else {
return (
<>
<p>
Component could not be found for blok{" "}
<strong>{blok.component}</strong>! Is it configured correctly?
Component could not be found for blok
{' '}
<strong>{blok.component}</strong>
! Is it configured correctly?
</p>
</>
);
}
}

return <div></div>;
}
},
);

StoryblokComponent.displayName = "StoryblokComponent";
SbServerComponent.displayName = 'StoryblokComponent';

export default StoryblokComponent;
export default SbServerComponent;
2 changes: 1 addition & 1 deletion lib/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default defineConfig({
},
},
rollupOptions: {
external: ['react'],
external: ['react', 'next'],
output: {
preserveModules: true,
globals: { react: 'React' },
Expand Down
Loading

0 comments on commit b6293aa

Please sign in to comment.