utils.setData vs useState for client-side state

Are there any known downsides to using utils.setData over useState to manage client state of objects.
// ...
onInput={(e) => {
const newName = e.currentTarget.value;
utils.application.getById.setData(
applicationId,
(prev) => {
if (!prev) return prev;
return { ...prev, name: newName };
},
);
}}
//...
// vs
//...
onInput={(e) => {
const newName = e.currentTarget.value;
setClientApplication((prev) => {
if (!prev) return prev;
return {...prev, name: newName};
})
}
///...
// ...
onInput={(e) => {
const newName = e.currentTarget.value;
utils.application.getById.setData(
applicationId,
(prev) => {
if (!prev) return prev;
return { ...prev, name: newName };
},
);
}}
//...
// vs
//...
onInput={(e) => {
const newName = e.currentTarget.value;
setClientApplication((prev) => {
if (!prev) return prev;
return {...prev, name: newName};
})
}
///...
12 Replies
>  /dev/trivee/
> /dev/trivee/2mo ago
Without knowing how utils.setData is implemented, potential downsides compared to useState could include less predictable re-renders, difficulty integrating with React's lifecycle, and a lack of built-in features like batch updates.
SharpieMaster
SharpieMasterOP2mo ago
utils is from trpc
>  /dev/trivee/
> /dev/trivee/2mo ago
Alright, if that's the case then: if the name field is directly tied to server data fetched via tRPC, use utils.application.getById.setData to update the cache and keep client/server in sync. However, if the name field is purely for client-side UI state and doesn't need to be persisted or reflected on the server, useState might be a better choice due to its simplicity
SharpieMaster
SharpieMasterOP2mo ago
in?
>  /dev/trivee/
> /dev/trivee/2mo ago
edited :) Might've forgotten to paste it from my clipboard xD srry abt that
X4
X42mo ago
/dev/trivee are you just copy pasting Sharpie's questions into an LLM? Why? @SharpieMaster It really depends on which other parts of your app are also paying attention to the value stored in your application.getById query cache, and also on if/when you want to send the data over the network to update your backend. If you ever had 2 components on-screen which both displayed the name, (e.g. there's a text box to edit it and also a banner at the top of the page displaying the application info) - do you want both of these strings to re-render on every keystroke? Or do you want the content of the text input to change but other components to stay the same until the user has finished typing? When the user finishes typing into the "set name" input, are you expecting them to then press a "submit" button which sends this information back to your server? If that the network request fails, what do you want to happen then? Should the name displayed revert back to what it was before they edited it?
SharpieMaster
SharpieMasterOP2mo ago
const utils = api.useUtils();
const { data: application } = api.application.getById.useQuery(
applicationId,
{
initialData: defaultApplication,
},
);
const [serverApplication, setServerApplication] =
useState(defaultApplication);

const { mutate: updateApplication } = api.application.update.useMutation({
onMutate: (data) => {
utils.application.getById.setData(applicationId, (prev) => {
if (!prev) return prev;
return { ...prev, ...data };
});

setServerApplication((prev) => ({
...prev,
...data,
}));
},

onSuccess: (data) => {
setServerApplication(data);
},
});

const [anyChanges, setAnyChanges] = useState(false);

useEffect(() => {
if (!application) return;

const hasChanges =
application.name !== serverApplication.name ||
application.description !== serverApplication.description ||
application.logoUrl !== serverApplication.logoUrl ||
application.redirectUri !== serverApplication.redirectUri;

setAnyChanges(hasChanges);
}, [application, defaultApplication]);
const utils = api.useUtils();
const { data: application } = api.application.getById.useQuery(
applicationId,
{
initialData: defaultApplication,
},
);
const [serverApplication, setServerApplication] =
useState(defaultApplication);

const { mutate: updateApplication } = api.application.update.useMutation({
onMutate: (data) => {
utils.application.getById.setData(applicationId, (prev) => {
if (!prev) return prev;
return { ...prev, ...data };
});

setServerApplication((prev) => ({
...prev,
...data,
}));
},

onSuccess: (data) => {
setServerApplication(data);
},
});

const [anyChanges, setAnyChanges] = useState(false);

useEffect(() => {
if (!application) return;

const hasChanges =
application.name !== serverApplication.name ||
application.description !== serverApplication.description ||
application.logoUrl !== serverApplication.logoUrl ||
application.redirectUri !== serverApplication.redirectUri;

setAnyChanges(hasChanges);
}, [application, defaultApplication]);
Its basically just so I can use the data from the api.application.getById.useQuery, instead of a useState + useEffect that looks at the data from that api call and updates the state when it changes. But even if I used useState additional re-renders would still be a concern right? If I wanted to avoid that id have to store each attribute as its own state, right? (Which seems kind of tedious)
X4
X42mo ago
In that example there I think instead of using a useState and a useEffect for anyChanges you should just say:
const anyChanges = application && (
application.name !== serverApplication.name ||
application.description !== serverApplication.description ||
application.logoUrl !== serverApplication.logoUrl ||
application.redirectUri !== serverApplication.redirectUri
);
const anyChanges = application && (
application.name !== serverApplication.name ||
application.description !== serverApplication.description ||
application.logoUrl !== serverApplication.logoUrl ||
application.redirectUri !== serverApplication.redirectUri
);
at the root level of the render function. I think your code might also be conflating 2 separate things in the useMutation hook: - Which application is selected in the frontend (your serverApplication useState) - The server state of that application Would it make sense to rearrange things so that these 2 concepts are more separate? Where does applicationId come from? When is it changing? Also why do you need anyChanges for? To show some kind of loading indicator in the UI? Would anything break if you simplified all your code above to just:
const utils = api.useUtils();
const { data: application } = api.application.getById.useQuery(
applicationId,
{ initialData: defaultApplication }
);

const { mutate: updateApplication } = api.application.update.useMutation({
onMutate: async (newData) => {
utils.application.getById.cancel(applicationId); // This wasn't in your exmaple before either but I assume you want it?
utils.application.getById.setData(applicationId, (prev) =>
prev ? { ...prev, ...newData } : prev
);
},
});
const utils = api.useUtils();
const { data: application } = api.application.getById.useQuery(
applicationId,
{ initialData: defaultApplication }
);

const { mutate: updateApplication } = api.application.update.useMutation({
onMutate: async (newData) => {
utils.application.getById.cancel(applicationId); // This wasn't in your exmaple before either but I assume you want it?
utils.application.getById.setData(applicationId, (prev) =>
prev ? { ...prev, ...newData } : prev
);
},
});
And all old consumers of serverApplication look at application instead?
SharpieMaster
SharpieMasterOP2mo ago
I agree with the change about anyChanges, this is an edit page, so any changes is used to see if the save button should be displayed or not, since if no changes were made, it wouldn’t make sense to have it there. applicationId is passed from a page param from the page component. It never changes on a page I’ll take a look at the changes in a bit and get back to you
X4
X42mo ago
If this is an edit page with a "save" button I'd personally recommend not mixing up your react query cache and the client side "unsaved changes" state. Something which might be adding to the confusion here is when you say:
const { data: application } = api.application.getById.useQuery(
applicationId,
{
initialData: defaultApplication,
},
);
const [serverApplication, setServerApplication] =
useState(defaultApplication);
const { data: application } = api.application.getById.useQuery(
applicationId,
{
initialData: defaultApplication,
},
);
const [serverApplication, setServerApplication] =
useState(defaultApplication);
It sounds like the names are backwards. The value of application is coming from react query (fetched from the server) and the value of serverApplication is client side state When a user is editing fields but hasn't pressed "save" yet, you should leave the state stored in the global react query cache alone, and just update the purely client side state that's only accessible from inside edit component.
SharpieMaster
SharpieMasterOP2mo ago
the purpose of the serverApplication is to store the last data that the server returned, its only changed when data comes from the server. And the application is used to track changes made on the client side using setData() also I dont think a cancel is necessary, since it never gets re-requested
const utils = api.useUtils();
const { data: serverApplication } = api.application.getById.useQuery(
applicationId,
{
initialData: defaultApplication,
},
);
const [clientApplication, setClientApplication] =
useState(defaultApplication);

const { mutate: updateApplication } = api.application.update.useMutation({
onMutate: (data) => {
const mutationData = {
...data,
description:
data.description === undefined ? null : data.description,
logoUrl: data.logoUrl === undefined ? null : data.logoUrl,
redirectUri:
data.redirectUri === undefined ? null : data.redirectUri,
};

utils.application.getById.setData(applicationId, (prev) => {
if (!prev) return prev;
return { ...prev, ...mutationData };
});
},

onSuccess: (data) => {
utils.application.getById.setData(applicationId, (prev) => {
if (!prev) return prev;
return { ...prev, ...data };
});
},
});

const anyChanges =
clientApplication &&
(clientApplication.name !== serverApplication?.name ||
clientApplication.description !== serverApplication?.description ||
clientApplication.logoUrl !== serverApplication?.logoUrl ||
clientApplication.redirectUri !== serverApplication?.redirectUri);
const utils = api.useUtils();
const { data: serverApplication } = api.application.getById.useQuery(
applicationId,
{
initialData: defaultApplication,
},
);
const [clientApplication, setClientApplication] =
useState(defaultApplication);

const { mutate: updateApplication } = api.application.update.useMutation({
onMutate: (data) => {
const mutationData = {
...data,
description:
data.description === undefined ? null : data.description,
logoUrl: data.logoUrl === undefined ? null : data.logoUrl,
redirectUri:
data.redirectUri === undefined ? null : data.redirectUri,
};

utils.application.getById.setData(applicationId, (prev) => {
if (!prev) return prev;
return { ...prev, ...mutationData };
});
},

onSuccess: (data) => {
utils.application.getById.setData(applicationId, (prev) => {
if (!prev) return prev;
return { ...prev, ...data };
});
},
});

const anyChanges =
clientApplication &&
(clientApplication.name !== serverApplication?.name ||
clientApplication.description !== serverApplication?.description ||
clientApplication.logoUrl !== serverApplication?.logoUrl ||
clientApplication.redirectUri !== serverApplication?.redirectUri);
This is the updated code, ive basically just swapped them. Thx for the help! I just have a general question, is there a way to access utils from the server side, so that I can manually add stuff to the cache when, for example, creating a new application (it would cache it for the next get request so that it doesnt have to query the database)
X4
X42mo ago
I don't think that's possible. utils (the react query cache) only exists on the client side, and if you have multiple users opening the app at the same time, they can all have different data stored in there But if what you're trying to achieve is, for a single user, they can click a button to edit a thing, and then they immediately see that thing updated in their UI without needing to wait to re-fetch the data they just edited this is very possible (and should be happening for you by default already with the code you shared above)

Did you find this page helpful?