feat(website): Throttle playground URL update
MODIFY
website/src/playground/PlaygroundLoader.tsx
website/src/playground/PlaygroundLoader.tsx
@@ -35,7 +35,7 @@ function throttle(callback: () => void): () => void {
};
}
-function App() {
+function PlaygroundLoader() {
const [loadingState, setLoadingState] = useState(LoadingState.Loading);
const [state, setPlaygroundState, resetPlaygroundState] =
usePlaygroundState();
@@ -189,29 +189,70 @@ function App() {
}
}
-const lastSearchStore = createLocalStorage("last-search");
+function buildLocation(state: PlaygroundState): string {
+ const rawQueryParams: Record<string, unknown> = {
+ ...state.settings,
+ };
-const FILE_QUERY_KEY_REGEX = /^files\.(.*?)$/;
+ // Eliminate default values
+ const queryStringObj: Record<string, string> = {};
+ for (const key in rawQueryParams) {
+ const defaultValue = String(
+ defaultPlaygroundState.settings[key as keyof PlaygroundSettings],
+ );
+ const rawValue = rawQueryParams[key];
+ const value = String(rawValue);
-export function usePlaygroundState(): [
- PlaygroundState,
- Dispatch<SetStateAction<PlaygroundState>>,
- () => void,
-] {
- const searchQuery =
- window.location.search === ""
- ? lastSearchStore.get() ?? ""
- : window.location.search;
+ if (rawValue !== undefined && value !== defaultValue) {
+ queryStringObj[key] = value;
+ }
+ }
+
+ if (state.singleFileMode && Object.keys(state.files).length === 1) {
+ // Single file mode
+ const code = getCurrentCode(state);
+ if (code) {
+ queryStringObj.code = encodeCode(code);
+ }
+
+ if (!isTypeScriptFilename(state.currentFile)) {
+ queryStringObj.typescript = "false";
+ }
+
+ if (!isJSXFilename(state.currentFile)) {
+ queryStringObj.jsx = "false";
+ }
+
+ if (isScriptFilename(state.currentFile)) {
+ queryStringObj.script = "true";
+ }
+ } else {
+ // Populate files
+ for (const filename in state.files) {
+ const content = state.files[filename]?.content ?? "";
+ queryStringObj[`files.${filename}`] = encodeCode(content);
+ }
+ }
+
+ const queryString = new URLSearchParams(queryStringObj).toString();
+ lastSearchStore.set(queryString);
+
+ let url = `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
+ if (queryString !== "") {
+ url += `?${queryString}`;
+ }
+ return url;
+}
function initState(
searchParams: URLSearchParams,
- ignoreFiles: boolean,
+ includeFiles: boolean,
): PlaygroundState {
let singleFileMode = true;
let hasFiles = false;
let files: PlaygroundState["files"] = {};
- if (!ignoreFiles) {
+ if (includeFiles) {
// Populate files
for (const [key, value] of searchParams) {
const match = key.match(FILE_QUERY_KEY_REGEX);
@@ -287,74 +328,54 @@ export function usePlaygroundState(): [
};
}
- const [playgroundState, setPlaygroundState] = useState(() =>
- initState(new URLSearchParams(searchQuery), window.location.search === ""),
- );
-
- function resetPlaygroundState() {
- setPlaygroundState(initState(new URLSearchParams(""), false));
- }
+const lastSearchStore = createLocalStorage("last-search");
- useEffect(() => {
- const rawQueryParams: Record<string, unknown> = {
- ...playgroundState.settings,
- };
+const FILE_QUERY_KEY_REGEX = /^files\.(.*?)$/;
- // Eliminate default values
- const queryStringObj: Record<string, string> = {};
- for (const key in rawQueryParams) {
- const defaultValue = String(
- defaultPlaygroundState.settings[key as keyof PlaygroundSettings],
- );
- const rawValue = rawQueryParams[key];
- const value = String(rawValue);
+// Safari/Webkit/JSC/whatever only allows setting a URL 50 times within 30 seconds
+// set our maximum update frequency just under that to avoid any chance of hitting it
+const URL_UPDATE_THROTTLE = 30000 / 40;
- if (rawValue !== undefined && value !== defaultValue) {
- queryStringObj[key] = value;
- }
- }
+export function usePlaygroundState(): [
+ PlaygroundState,
+ Dispatch<SetStateAction<PlaygroundState>>,
+ () => void,
+] {
+ const [url, setURL] = useState(window.location.toString());
- if (
- playgroundState.singleFileMode &&
- Object.keys(playgroundState.files).length === 1
- ) {
- // Single file mode
- const code = getCurrentCode(playgroundState);
- if (code) {
- queryStringObj.code = encodeCode(code);
- }
+ const [playgroundState, setPlaygroundState] = useState(() => {
+ let searchQuery = window.location.search;
+ let includeSearchQueryFiles = true;
- if (!isTypeScriptFilename(playgroundState.currentFile)) {
- queryStringObj.typescript = "false";
+ // Default to query of last session to load settings
+ if (searchQuery === "") {
+ searchQuery = lastSearchStore.get() ?? "";
+ includeSearchQueryFiles = false;
}
- if (!isJSXFilename(playgroundState.currentFile)) {
- queryStringObj.jsx = "false";
- }
+ return initState(new URLSearchParams(searchQuery), includeSearchQueryFiles);
+ });
- if (isScriptFilename(playgroundState.currentFile)) {
- queryStringObj.script = "true";
- }
- } else {
- // Populate files
- for (const filename in playgroundState.files) {
- const content = playgroundState.files[filename]?.content ?? "";
- queryStringObj[`files.${filename}`] = encodeCode(content);
- }
+ function resetPlaygroundState() {
+ setPlaygroundState(initState(new URLSearchParams(""), false));
}
- const queryString = new URLSearchParams(queryStringObj).toString();
- lastSearchStore.set(queryString);
-
- let url = `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
- if (queryString !== "") {
- url += `?${queryString}`;
- }
+ useEffect(() => {
+ setURL(buildLocation(playgroundState));
+ }, [playgroundState]);
+ // Throttle updating of URL
+ useEffect(() => {
+ const timeout = setTimeout(() => {
window.history.replaceState({ path: url }, "", url);
- }, [playgroundState]);
+ }, URL_UPDATE_THROTTLE);
+
+ return () => {
+ clearTimeout(timeout);
+ };
+ }, [url]);
return [playgroundState, setPlaygroundState, resetPlaygroundState];
}
-export default App;
+export default PlaygroundLoader;

MODIFY
website/src/playground/utils.ts
website/src/playground/utils.ts
@@ -56,6 +56,7 @@ export function useWindowSize(): Size {
width: undefined,
height: undefined,
});
+
useEffect(() => {
// Handler to call on window resize
function handleResize() {
@@ -65,11 +66,16 @@ export function useWindowSize(): Size {
// Add event listener
window.addEventListener("resize", handleResize);
+
// Call handler right away so state gets updated with initial window size
handleResize();
+
// Remove event listener on cleanup
- return () => window.removeEventListener("resize", handleResize);
+ return () => {
+ window.removeEventListener("resize", handleResize);
+ };
}, []); // Empty array ensures that effect is only run on mount
+
return windowSize;
}
@@ -220,7 +226,7 @@ export function decodeCode(encoded: string): string {
try {
return fromBinary(atob(encoded));
} catch {
- return "";
+ return encoded;
}
}

feat(website): Throttle playground URL update (#3821)