Skip to content

Commit

Permalink
Fix fast-refresh and URL updates when using preview
Browse files Browse the repository at this point in the history
  • Loading branch information
jakub-gonet committed Jun 27, 2024
1 parent 62a9aba commit f9dbad6
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 72 deletions.
23 changes: 19 additions & 4 deletions packages/vscode-extension/lib/expo_router_plugin.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { useSyncExternalStore, useEffect } from "react";
import { useRouter } from "expo-router";
import { store } from "expo-router/build/global-state/router-store.js";

import { useSyncExternalStore, useEffect, useState } from "react";
import { useRouter } from "expo-router";

function computeRouteIdentifier(pathname, params) {
return pathname + JSON.stringify(params);
}

function useRouterPluginMainHook({ onNavigationChange }) {
const router = useRouter();
const [initCallbacks, setInitCallbacks] = useState([]);
const routeInfo = useSyncExternalStore(
store.subscribeToRootState,
store.routeInfoSnapshot,
store.routeInfoSnapshot
);

const pathname = routeInfo?.pathname;
const params = routeInfo?.params;
useEffect(() => {
Expand All @@ -23,6 +26,16 @@ function useRouterPluginMainHook({ onNavigationChange }) {
id: computeRouteIdentifier(pathname, params),
});
}, [pathname, params]);

useEffect(() => {
if (router.navigationRef) {
for (const callback of initCallbacks) {
callback();
}
setInitCallbacks([]);
}
}, [router.navigationRef]);

return {
getCurrentNavigationDescriptor: () => {
const snapshot = store.routeInfoSnapshot();
Expand All @@ -33,9 +46,11 @@ function useRouterPluginMainHook({ onNavigationChange }) {
id: computeRouteIdentifier(snapshot.pathname, snapshot.params),
};
},
onRouterInitialization: (fn) => {
setInitCallbacks((callbacks) => [fn, ...callbacks]);
},
requestNavigationChange: ({ pathname, params }) => {
router.navigate(pathname);
router.setParams(params);
router.push(pathname, params);
},
};
}
Expand Down
20 changes: 18 additions & 2 deletions packages/vscode-extension/lib/expo_router_v2_plugin.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { useSyncExternalStore, useEffect } from "react";
import { useRouter } from "expo-router";
import { store } from "expo-router/src/global-state/router-store";

import { useSyncExternalStore, useEffect, useState } from "react";
import { useRouter } from "expo-router";

function computeRouteIdentifier(pathname, params) {
return pathname + JSON.stringify(params);
}

function useRouterPluginMainHook({ onNavigationChange }) {
const router = useRouter();
const [initCallbacks, setInitCallbacks] = useState([]);
const routeInfo = useSyncExternalStore(
store.subscribeToRootState,
store.routeInfoSnapshot,
store.routeInfoSnapshot
);

const pathname = routeInfo?.pathname;
const params = routeInfo?.params;
useEffect(() => {
Expand All @@ -23,6 +26,16 @@ function useRouterPluginMainHook({ onNavigationChange }) {
id: computeRouteIdentifier(pathname, params),
});
}, [pathname, params]);

useEffect(() => {
if (router.navigationRef) {
for (const callback of initCallbacks) {
callback();
}
setInitCallbacks([]);
}
}, [router.navigationRef]);

return {
getCurrentNavigationDescriptor: () => {
const snapshot = store.routeInfoSnapshot();
Expand All @@ -33,6 +46,9 @@ function useRouterPluginMainHook({ onNavigationChange }) {
id: computeRouteIdentifier(snapshot.pathname, snapshot.params),
};
},
onRouterInitialization: (fn) => {
setInitCallbacks((callbacks) => [fn, ...callbacks]);
},
requestNavigationChange: ({ pathname, params }) => {
router.push(pathname, params);
},
Expand Down
39 changes: 17 additions & 22 deletions packages/vscode-extension/lib/preview.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,31 @@
const { AppRegistry, View } = require("react-native");

global.__RNIDE_previews ||= new Map();
export const PREVIEW_APP_KEY = "RNIDE_preview";

function stringifyProps(obj) {
const keyValuePairs = [];
global.__RNIDE_previews ||= new Map();

for (let key in obj) {
if (obj.hasOwnProperty(key)) {
keyValuePairs.push(`${key}=${obj[key]}`);
}
export function Preview({ previewKey }) {
const previewData = global.__RNIDE_previews.get(previewKey);
if (!previewData || !previewData.component) {
return null;
}

return keyValuePairs.join(" ");
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
{previewData.component}
</View>
);
}

export function preview(component) {
if (component === undefined || component._source === null) {
if (!component || component._source === null) {
return;
}

const name = `preview:/${component._source.fileName}:${component._source.lineNumber}`;
function Preview() {
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>{component}</View>
);
}
global.__RNIDE_previews.set(name, {
appKey: name,
const key = `preview:/${component._source.fileName}:${component._source.lineNumber}`;
global.__RNIDE_previews.set(key, {
component,
name: component.type.name,
props: stringifyProps(component.props),
fileName: component._source.fileName,
lineNumber: component._source.lineNumber,
});
AppRegistry.registerComponent(name, () => Preview);
}

AppRegistry.registerComponent(PREVIEW_APP_KEY, () => Preview);
71 changes: 29 additions & 42 deletions packages/vscode-extension/lib/wrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const {
Linking,
findNodeHandle,
} = require("react-native");
const { PREVIEW_APP_KEY } = require("./preview");

const navigationPlugins = [];
export function registerNavigationPlugin(name, plugin) {
Expand All @@ -16,19 +17,11 @@ export function registerNavigationPlugin(name, plugin) {

let navigationHistory = new Map();

function isPreviewUrl(url) {
return url.startsWith("preview://");
}

function getCurrentSceneName() {
function getCurrentScene() {
const SceneTracker = require("react-native/Libraries/Utilities/SceneTracker");
return SceneTracker.getActiveScene().name;
}

function isRunningPreview() {
return isPreviewUrl(getCurrentSceneName());
}

function emptyNavigationHook() {
return {
getCurrentNavigationDescriptor: () => undefined,
Expand All @@ -47,7 +40,9 @@ function useAgentListener(agent, eventName, listener, deps = []) {
}, [agent, ...deps]);
}

export function PreviewAppWrapper({ children, ...rest }) {
let currentPreviewKey = undefined;

export function PreviewAppWrapper({ children, ..._rest }) {
const rootTag = useContext(RootTagContext);
const [devtoolsAgent, setDevtoolsAgent] = useState(null);
const [hasLayout, setHasLayout] = useState(false);
Expand All @@ -56,33 +51,34 @@ export function PreviewAppWrapper({ children, ...rest }) {
const handleNavigationChange = useCallback(
(navigationDescriptor) => {
navigationHistory.set(navigationDescriptor.id, navigationDescriptor);
devtoolsAgent &&
devtoolsAgent._bridge.send("RNIDE_navigationChanged", {
displayName: navigationDescriptor.name,
id: navigationDescriptor.id,
});
devtoolsAgent?._bridge.send("RNIDE_navigationChanged", {
displayName: navigationDescriptor.name,
id: navigationDescriptor.id,
});
},
[devtoolsAgent]
);

const useNavigationMainHook =
(navigationPlugins.length && navigationPlugins[0].plugin.mainHook) || emptyNavigationHook;
const { requestNavigationChange } = useNavigationMainHook({
const useNavigationMainHook = navigationPlugins[0]?.plugin.mainHook || emptyNavigationHook;
const { requestNavigationChange, onRouterInitialization } = useNavigationMainHook({
onNavigationChange: handleNavigationChange,
});

const openPreview = useCallback(
(previewKey) => {
AppRegistry.runApplication(previewKey, {
currentPreviewKey = previewKey;
AppRegistry.runApplication(PREVIEW_APP_KEY, {
rootTag,
initialProps: {},
initialProps: { previewKey },
});
const preview = global.__RNIDE_previews.get(previewKey);
handleNavigationChange({ id: previewKey, name: `preview:${preview.name}` });
},
[rootTag]
[rootTag, devtoolsAgent]
);

const closePreview = useCallback(() => {
if (isRunningPreview()) {
if (currentPreviewKey) {
AppRegistry.runApplication("main", {
rootTag,
initialProps: {},
Expand Down Expand Up @@ -114,13 +110,16 @@ export function PreviewAppWrapper({ children, ...rest }) {
devtoolsAgent,
"RNIDE_openNavigation",
(payload) => {
if (isPreviewUrl(payload.id)) {
const isPreviewUrl = payload.id.startsWith("preview://");
if (isPreviewUrl) {
openPreview(payload.id);
return;
}
closePreview();
const navigationDescriptor = navigationHistory.get(payload.id);
navigationDescriptor && requestNavigationChange(navigationDescriptor);
onRouterInitialization(() => {
const navigationDescriptor = navigationHistory.get(payload.id);
navigationDescriptor && requestNavigationChange(navigationDescriptor);
});
},
[openPreview, closePreview, requestNavigationChange]
);
Expand Down Expand Up @@ -212,10 +211,6 @@ export function PreviewAppWrapper({ children, ...rest }) {
};
LoadingView.hide = () => {
devtoolsAgent._bridge.send("RNIDE_fastRefreshComplete");
if (isRunningPreview()) {
// refresh preview component
openPreview(getCurrentSceneName());
}
};
}
}, [devtoolsAgent]);
Expand All @@ -235,12 +230,14 @@ export function PreviewAppWrapper({ children, ...rest }) {

useEffect(() => {
if (!!devtoolsAgent && hasLayout) {
const SceneTracker = require("react-native/Libraries/Utilities/SceneTracker");
const sceneName = SceneTracker.getActiveScene().name;
const appKey = getCurrentScene();
devtoolsAgent._bridge.send("RNIDE_appReady", {
appKey: sceneName,
appKey,
navigationPlugins: navigationPlugins.map((plugin) => plugin.name),
});
if (appKey === "main") {
currentPreviewKey = undefined;
}
}
}, [!!devtoolsAgent && hasLayout]);

Expand All @@ -250,16 +247,6 @@ export function PreviewAppWrapper({ children, ...rest }) {
style={{ flex: 1 }}
onLayout={() => {
setHasLayout(true);
if (devtoolsAgent) {
if (isRunningPreview()) {
const sceneName = getCurrentSceneName();
const preview = (global.__RNIDE_previews || new Map()).get(sceneName);
devtoolsAgent._bridge.send("RNIDE_navigationChanged", {
displayName: `preview:${preview.name}`, // TODO: make names unique if there are multiple previews of the same component
id: sceneName,
});
}
}
}}>
{children}
</View>
Expand Down
6 changes: 4 additions & 2 deletions packages/vscode-extension/src/webview/components/UrlBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ interface UrlBarProps {

function UrlBar({ project, disabled }: UrlBarProps) {
const [urlList, setUrlList] = useState<{ name: string; id: string }[]>([]);

useEffect(() => {
function handleNavigationChanged(navigationData: { displayName: string; id: string }) {
const newRecord = { name: navigationData.displayName, id: navigationData.id };
Expand Down Expand Up @@ -57,7 +56,10 @@ function UrlBar({ project, disabled }: UrlBarProps) {
<span className="codicon codicon-refresh" />
</IconButton>
<IconButton
onClick={() => project.goHome()}
onClick={() => {
project.goHome();
setUrlList([]);
}}
tooltip={{
label: "Go to main screen",
side: "bottom",
Expand Down

0 comments on commit f9dbad6

Please sign in to comment.