T
TanStack9mo ago
eastern-cyan

Why does my form default value not re-render?

export const Route = createFileRoute('/product-backlog/task/edit/$taskID')({
component: EditTask,
loader: ({ params }) => {
return fetchTask(params.taskID)
}
})

function EditTask() {
const task = Route.useLoaderData()
const { taskID } = Route.useParams()

const navigate = useNavigate({ from: "/product-backlog/task/edit/$taskID" })
const navigateTo = () => {
navigate({ to: "/product-backlog" })
}

const editTask = async (formData: FormData) => {
// Edit Task
navigateTo()
}

return <TaskEditor task={task} action={editTask} navigateTo={navigateTo} deleteTask={deleteTask} />
}
export const Route = createFileRoute('/product-backlog/task/edit/$taskID')({
component: EditTask,
loader: ({ params }) => {
return fetchTask(params.taskID)
}
})

function EditTask() {
const task = Route.useLoaderData()
const { taskID } = Route.useParams()

const navigate = useNavigate({ from: "/product-backlog/task/edit/$taskID" })
const navigateTo = () => {
navigate({ to: "/product-backlog" })
}

const editTask = async (formData: FormData) => {
// Edit Task
navigateTo()
}

return <TaskEditor task={task} action={editTask} navigateTo={navigateTo} deleteTask={deleteTask} />
}
function TaskEditor(props: TaskEditorProps) {

return (
<section>
<form action={props.action}>
<input defaultValue={props.task?.name || ""} />
<button>Save</button>
</form>
</section>
)
}

export default TaskEditor
function TaskEditor(props: TaskEditorProps) {

return (
<section>
<form action={props.action}>
<input defaultValue={props.task?.name || ""} />
<button>Save</button>
</form>
</section>
)
}

export default TaskEditor
Initially, I am in "/product-backlog" route. I navigate to "/product-backlog/task/edit/1" to edit my task name. Once I click the "Save" button, the edit is successful and I am able to observe the changes in my "/product-backlog" route. However, when I navigate back to "/product-backlog/task/edit/1", the default values of <input /> still shows the previous value, but if I were to navigate to "/product-backlog" and back to "/product-backlog/task/edit/1" again, this time the default values of <input /> shows the updated value. I tried console.log() the props.task in <TaskEditor /> and I see that it logs 2 different values, the old value and the new value (2 times old, 2 times new in strict mode), while displaying the old value. I thought by navigating from "/product-backlog/task/edit/1" to "/product-backlog" will cause my <TaskEditor /> to unmount, and so navigating back to "/product-backlog/task/edit/1" will cause a re-render and display the correct value but it didn't Can anyone explain to me what is happening behind the scenes? Thank you
17 Replies
grumpy-cyan
grumpy-cyan9mo ago
please provide a complete minimal example by forking one of the existing stackblitz examples
eastern-cyan
eastern-cyanOP9mo ago
@Manuel Schiller yes of course sorry
eastern-cyan
eastern-cyanOP9mo ago
eastern-cyan
eastern-cyanOP9mo ago
clicking on the pink task will navigate to the form route and after changing the value by clicking submit it will navigate back to the product backlog route and the new value can be seen but if I were to press the same task that I change in the form route I will see the old values but if I click cancel and then go back to the form route by clicking the same task again the new value can now be seen
grumpy-cyan
grumpy-cyan9mo ago
you need to invalidate the loader data
grumpy-cyan
grumpy-cyan9mo ago
grumpy-cyan
grumpy-cyan9mo ago
const editTask = async (formData: FormData) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
const index = tasks.findIndex((task) => task.taskID === taskID);
delete tasks[index];

const name = formData.get('name') as string;
tasks[index] = { taskID, name };
router.invalidate(); // <<<<-------
navigateTo();
};
const editTask = async (formData: FormData) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
const index = tasks.findIndex((task) => task.taskID === taskID);
delete tasks[index];

const name = formData.get('name') as string;
tasks[index] = { taskID, name };
router.invalidate(); // <<<<-------
navigateTo();
};
otherwise the loader does not rerun and router will use the stale values
eastern-cyan
eastern-cyanOP9mo ago
@Manuel Schillerohh I see but when exactly does the loader rerun, cause if i were to put console.log() in my TaskEditor component, after I change the value and return to the form to see my old value, the console first log the old value and then the new value
grumpy-cyan
grumpy-cyan9mo ago
it reruns when router.invalidate is executed you can also await it
eastern-cyan
eastern-cyanOP9mo ago
I meant like when router.invalidate() is not present though
grumpy-cyan
grumpy-cyan9mo ago
await router.invalidate({sync: true});
await router.invalidate({sync: true});
then it would only re-run when the stale time is reached
eastern-cyan
eastern-cyanOP9mo ago
@Manuel Schilleroh okayy, thank you so much for taking the time
grumpy-cyan
grumpy-cyan9mo ago
this can also be nicely inspected via the devtools
No description
grumpy-cyan
grumpy-cyan9mo ago
check the "cached matches" section I think I misspoke here what you see is routers 'stale-while-revalidate' behavior so you navigate to a cached route, then the previous loader data is returned to your component then the loader executes in the background, potentially updating the data so it looks like your TaskEditor is not properly re-rendering when the new data arrives by invalidating, you force the loader to re-execute when navigating there, so it solves the issue at hand but at the cost of more latency
eastern-cyan
eastern-cyanOP9mo ago
ohh okay that makes more sense thanks
grumpy-cyan
grumpy-cyan9mo ago
a solution to force re-rendering of that default value:
<input defaultValue={props.task?.name} key={props.task?.name} name="name" />
<input defaultValue={props.task?.name} key={props.task?.name} name="name" />
eastern-cyan
eastern-cyanOP9mo ago
alright will do, seems more efficient

Did you find this page helpful?