最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

reactjs - Remix (React) Waiting for Submit to complete without useMemo - Stack Overflow

programmeradmin2浏览0评论

I have a table where the last column is a button that triggers an action which in turn queries the DB, returns a result and downloads the result as a csv. In order for the data to be processed and the download the start, I need the submit to complete so there is (correct) data to process.

Returning a promise from submit has been discussed before here. There is a nice solution there, however it uses useMemo which persists the evaluated result of the promise, instead of restarting the process on each submit. This doesn't work for my use case.

I tried two things

I tried two different things

  1. I tried adding an arbitrary dependency into useMemo and updating this value inside _submit = useCallback( ...) to force construction of a new Promise on each submit, however this doesn't seem to work

  2. I tried removing useMemo all together however this results in a strange behavior. The first click of a button, resolves the promise but doesn't actually trigger the .then() block. The second click returns the result from the first click, the third click from the second and so on.. Long story short, the downloaded data is dragging behind by one invocation. I haven't been able to track down the reason behind this

I modified the example in that discussion to reflect both of these issue. Namely, I am making the server return a random number Minimal reproduction:

.tsx

==> In this example, you can see that no matter how many times you click on the button, the same number is returned

==> If you instead change line 17 to const submit = useSubmitPromise2(); (a.k.a useSubmitPromise(); -> useSubmitPromise2(); you can see the value being alerted dragging behind by 1 and the initial button click not doing anything

How can I modify this code so that the result from the action is refreshed on every button click?

Full code:

import type { ActionArgs, LoaderArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import type { SubmitOptions } from '@remix-run/react';
import { useActionData, useNavigation, useSubmit } from '@remix-run/react';
import { useCallback, useEffect, useMemo } from 'react';

export function loader({ request }: LoaderArgs) {
  return json({ message: 'Hello World' });
}

export function action({ request }: ActionArgs) {
  let x = Math.random();
  console.log('generated ' + x);
  return json({ message: x });
}
export default function Index() {
  const submit = useSubmitPromise();

  const doSubmit = useCallback(
    (e: any) => {
      console.log('doSubmit');
      e.preventDefault();

      submit({ test: '123' }, { method: 'post', action: '?index' }).then(
        (data) => {
          console.log('result', data);
          alert('Resolved promise with:\n' + JSON.stringify(data, null, 2));
        }
      );
    },
    [submit]
  );

  return (
    <div>
      <h1>Welcome to Remix</h1>
      <button onClick={doSubmit}>useSubmitPromise</button>
    </div>
  );
}

// remix doesn't export this type
declare type SubmitTarget =
  | HTMLFormElement
  | HTMLButtonElement
  | HTMLInputElement
  | FormData
  | URLSearchParams
  | {
      [name: string]: string;
    }
  | null;

function useSubmitPromise() {
  const submit = useSubmit();
  const navigation = useNavigation();
  const actionData = useActionData();
  const $deferred = useMemo(() => deferred(), []);

  useEffect(() => {
    if (navigation.state === 'idle' && actionData) {
      $deferred.resolve(actionData);
    }
  }, [$deferred, navigation.state, actionData]);

  const _submit = useCallback(
    (target: SubmitTarget, options: SubmitOptions = {}) => {
      submit(target, options);
      return $deferred.promise;
    },
    [$deferred.promise, submit]
  );

  return _submit;
}

function useSubmitPromise2() {
  const submit = useSubmit();
  const navigation = useNavigation();
  const actionData = useActionData();
  const $deferred = deferred(); //useMemo(() => deferred(), []);

  useEffect(() => {
    if (navigation.state === 'idle' && actionData) {
      $deferred.resolve(actionData);
    }
  }, [$deferred, navigation.state, actionData]);

  const _submit = useCallback(
    (target: SubmitTarget, options: SubmitOptions = {}) => {
      submit(target, options);
      return $deferred.promise;
    },
    [$deferred.promise, submit]
  );

  return _submit;
}

// create a *deferred* promise
function deferred() {
  let resolve: typeof Promise.resolve;
  let reject: typeof Promise.reject;
  const promise = new Promise((res, rej) => {
    resolve = res;
    reject = rej;
  });

  return { resolve, reject, promise };
}

I have a table where the last column is a button that triggers an action which in turn queries the DB, returns a result and downloads the result as a csv. In order for the data to be processed and the download the start, I need the submit to complete so there is (correct) data to process.

Returning a promise from submit has been discussed before here. There is a nice solution there, however it uses useMemo which persists the evaluated result of the promise, instead of restarting the process on each submit. This doesn't work for my use case.

I tried two things

I tried two different things

  1. I tried adding an arbitrary dependency into useMemo and updating this value inside _submit = useCallback( ...) to force construction of a new Promise on each submit, however this doesn't seem to work

  2. I tried removing useMemo all together however this results in a strange behavior. The first click of a button, resolves the promise but doesn't actually trigger the .then() block. The second click returns the result from the first click, the third click from the second and so on.. Long story short, the downloaded data is dragging behind by one invocation. I haven't been able to track down the reason behind this

I modified the example in that discussion to reflect both of these issue. Namely, I am making the server return a random number Minimal reproduction:

https://stackblitz/edit/node-cohmdvxv?file=app%2Froutes%2Findex.tsx

==> In this example, you can see that no matter how many times you click on the button, the same number is returned

==> If you instead change line 17 to const submit = useSubmitPromise2(); (a.k.a useSubmitPromise(); -> useSubmitPromise2(); you can see the value being alerted dragging behind by 1 and the initial button click not doing anything

How can I modify this code so that the result from the action is refreshed on every button click?

Full code:

import type { ActionArgs, LoaderArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import type { SubmitOptions } from '@remix-run/react';
import { useActionData, useNavigation, useSubmit } from '@remix-run/react';
import { useCallback, useEffect, useMemo } from 'react';

export function loader({ request }: LoaderArgs) {
  return json({ message: 'Hello World' });
}

export function action({ request }: ActionArgs) {
  let x = Math.random();
  console.log('generated ' + x);
  return json({ message: x });
}
export default function Index() {
  const submit = useSubmitPromise();

  const doSubmit = useCallback(
    (e: any) => {
      console.log('doSubmit');
      e.preventDefault();

      submit({ test: '123' }, { method: 'post', action: '?index' }).then(
        (data) => {
          console.log('result', data);
          alert('Resolved promise with:\n' + JSON.stringify(data, null, 2));
        }
      );
    },
    [submit]
  );

  return (
    <div>
      <h1>Welcome to Remix</h1>
      <button onClick={doSubmit}>useSubmitPromise</button>
    </div>
  );
}

// remix doesn't export this type
declare type SubmitTarget =
  | HTMLFormElement
  | HTMLButtonElement
  | HTMLInputElement
  | FormData
  | URLSearchParams
  | {
      [name: string]: string;
    }
  | null;

function useSubmitPromise() {
  const submit = useSubmit();
  const navigation = useNavigation();
  const actionData = useActionData();
  const $deferred = useMemo(() => deferred(), []);

  useEffect(() => {
    if (navigation.state === 'idle' && actionData) {
      $deferred.resolve(actionData);
    }
  }, [$deferred, navigation.state, actionData]);

  const _submit = useCallback(
    (target: SubmitTarget, options: SubmitOptions = {}) => {
      submit(target, options);
      return $deferred.promise;
    },
    [$deferred.promise, submit]
  );

  return _submit;
}

function useSubmitPromise2() {
  const submit = useSubmit();
  const navigation = useNavigation();
  const actionData = useActionData();
  const $deferred = deferred(); //useMemo(() => deferred(), []);

  useEffect(() => {
    if (navigation.state === 'idle' && actionData) {
      $deferred.resolve(actionData);
    }
  }, [$deferred, navigation.state, actionData]);

  const _submit = useCallback(
    (target: SubmitTarget, options: SubmitOptions = {}) => {
      submit(target, options);
      return $deferred.promise;
    },
    [$deferred.promise, submit]
  );

  return _submit;
}

// create a *deferred* promise
function deferred() {
  let resolve: typeof Promise.resolve;
  let reject: typeof Promise.reject;
  const promise = new Promise((res, rej) => {
    resolve = res;
    reject = rej;
  });

  return { resolve, reject, promise };
}
Share Improve this question edited Mar 31 at 19:58 sinanspd asked Mar 31 at 19:56 sinanspdsinanspd 2,7343 gold badges21 silver badges39 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 0

Ok, so... figured out the solution. Two steps

  1. Use useState to keep the deferred promise const [currentDeferred, setCurrentDeferred] = useState(() => deferred());

  2. Remix seems to re-render twice for each submit. Which results in the downloaded data being duplicated. I manually kept track of the last fetched answer and compared it against the new one to force the deferred promise to be only resolved once. This is hacky, and not great (as the user might in fact want to download it twice) but oh well..

Final code

function useSubmitPromise() {
    const submit = useSubmit();
    const navigation = useNavigation();
    const actionData = useActionData();

    const [currentDeferred, setCurrentDeferred] = useState(() => deferred());
    const lastActionDataRef = useRef<any>(null);

    if (!currentDeferred) return;

    useEffect(() => {
      if (actionData && actionData !== lastActionDataRef.current) {
        currentDeferred.resolve(actionData);
        lastActionDataRef.current = actionData;
      }
    }, [currentDeferred, navigation.state, actionData, lastActionDataRef]);

    const _submit = useCallback(
      (target, options: SubmitOptions = {}) => {
        const newDeferred = deferred();
        setCurrentDeferred(newDeferred);
        submit(target, options);
        return newDeferred.promise;
      },
      [submit]
    );

    return _submit;
  }

发布评论

评论列表(0)

  1. 暂无评论