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
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 workI 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
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 workI 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
1 Answer
Reset to default 0Ok, so... figured out the solution. Two steps
Use
useState
to keep the deferred promiseconst [currentDeferred, setCurrentDeferred] = useState(() => deferred());
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;
}