facebookexperimental / Recoil

Recoil is an experimental state management library for React apps. It provides several capabilities that are difficult to achieve with React alone, while being compatible with the newest features of React.
https://recoiljs.org/
MIT License
19.5k stars 1.18k forks source link

<RecoilURLSync doesn't throttle calls to history.replace / history.pushState w/ Safari #1910

Open sbdchd opened 1 year ago

sbdchd commented 1 year ago

With Safari, if you call history.replace or history.pushState too frequently, the history APIs will throw a SecurityError.

SecurityError: Attempt to use history.replaceState() more than 100 times per 30 seconds

example code to reproduce:

import * as React from "react";
import ReactDOM from "react-dom/client";
import { RecoilRoot, atom, useRecoilState } from "recoil";
import { RecoilURLSyncJSON, syncEffect } from "recoil-sync";
import { string } from "@recoiljs/refine";

const textState = atom({
  key: "textState", // unique ID (with respect to other atoms/selectors)
  default: "", // default value (aka initial value)
  effects: [syncEffect({ refine: string() })],
});

function TextInput() {
  const [text, setText] = useRecoilState(textState);

  const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setText(event.target.value);
  };

  return (
    <div>
      <input type="text" value={text} onChange={onChange} />
      <br />
      Echo: {text}
    </div>
  );
}

export default function App() {
  return (
    <div>
      <TextInput />;
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById("root")!);
root.render(
  <React.StrictMode>
    <RecoilRoot>
      <RecoilURLSyncJSON location={{ part: "queryParams" }}>
        <App />
      </RecoilURLSyncJSON>
    </RecoilRoot>
  </React.StrictMode>
);
mondaychen commented 1 year ago

Ah interesting. Thanks for reporting! I'm wondering what might be the best solution here. A simple timed throttle on the atom won't work because 3.33 updates per second doesn't make sense for a text input. Top of my mind: option 1, we can create a real time state for the text input and put a throttle between the state and the atom (a solution external to recoil-sync and can be applied when needed); option 2, we can use a real time atom and put a throttle on the syncing effect (an API of recoil-sync). One problem here is that when the update happens on the other side (e.g. manual URL update) some data might be missing. But I guess that's usually fine for URL sync?