T
TanStack9mo ago
extended-salmon

Mutation to tackle state update race conditions. Good or bad idea?

Hello. I want to update state with some async result and wondering if this approach is good? If updateConfig called multiple times race condition can happen.
const useFinalConfig = (initialConfig: FinalConfig) => {
const [finalConfig, setFinalConfig] = useState(initialConfig);

const updateConfig = async (adjustableConfig: AdjustableConfig) => {
let config = mapAdjustableConffigToFinalConfig(adjustableConfig, finalConfig);
if (checkIsDifferent(config.importantOptions, finalConfig.importantOptions)) {
config = await buildFinalConfig(config);
}
setFinalConfig(finalConfig);
};

return { finalConfig, updateConfig };
};
const useFinalConfig = (initialConfig: FinalConfig) => {
const [finalConfig, setFinalConfig] = useState(initialConfig);

const updateConfig = async (adjustableConfig: AdjustableConfig) => {
let config = mapAdjustableConffigToFinalConfig(adjustableConfig, finalConfig);
if (checkIsDifferent(config.importantOptions, finalConfig.importantOptions)) {
config = await buildFinalConfig(config);
}
setFinalConfig(finalConfig);
};

return { finalConfig, updateConfig };
};
I decided to useMutation, because it has one special feature - you can use onSuccess option in mutation.mutate method to ensure than only last called mutation onSuccess will be called as per documentation, so I can set state in this success call.
const useFinalConfig = (initialConfig: FinalConfig) => {
const [finalConfig, setFinalConfig] = useState(initialConfig);

const finalConfigMutation = useMutation({
mutationFn: async ({
adjustableConfig,
currentConfig,
}: {
currentConfig: FinalConfig;
adjustableConfig: AdjustableConfig;
}) => {
let config = mapAdjustableConffigToFinalConfig(adjustableConfig, currentConfig);
if (checkIsDifferent(config.importantOptions, currentConfig.importantOptions)) {
config = await buildFinalConfig(config);
}
return config;
},
});

const updateConfig = async (adjustableConfig: AdjustableConfig) => {
finalConfigMutation.mutate(
{ currentConfig: finalConfig, adjustableConfig },
{ onSuccess: config => setFinalConfig(config) },
);
};

return { finalConfig, updateConfig };
const useFinalConfig = (initialConfig: FinalConfig) => {
const [finalConfig, setFinalConfig] = useState(initialConfig);

const finalConfigMutation = useMutation({
mutationFn: async ({
adjustableConfig,
currentConfig,
}: {
currentConfig: FinalConfig;
adjustableConfig: AdjustableConfig;
}) => {
let config = mapAdjustableConffigToFinalConfig(adjustableConfig, currentConfig);
if (checkIsDifferent(config.importantOptions, currentConfig.importantOptions)) {
config = await buildFinalConfig(config);
}
return config;
},
});

const updateConfig = async (adjustableConfig: AdjustableConfig) => {
finalConfigMutation.mutate(
{ currentConfig: finalConfig, adjustableConfig },
{ onSuccess: config => setFinalConfig(config) },
);
};

return { finalConfig, updateConfig };
Is this something mutation can be used for?
4 Replies
absent-sapphire
absent-sapphire9mo ago
Looks reasonably clever to me, shouldn't shoot you in the foot. Why is buildFinalConfig async? So the flow of what this is as far as I can understand it is 1. Initalize with a final config 2. Update with an ajustiable config patch 3. If "important options" have changed, do some async work 4. Return new final option If the async work in 3 isn't a side effect on a server or other part of your application, it can be cached with useQuery instead of dealing with useMutation So something like this instead, bear in mind this archecetured is a bit different to yours
const useFinalConfig = (baseConfig: BaseConfig) => {
const [baseConfig, setBaseConfig] = useState(baseConfig);

// treating the final config as a deriviation of the base config here, I'm sure this isn't right in your code
const finalConfigQuery = useQuery({
queryKey: ['final-config', baseConfig],
queryFn: () => {
return buildFinalConfig(config);
}
});

const updateBaseConfig = (adjustableBaseConfig: AdjustableConfig) => {
const newBaseConfig = mapAdjustableConffigToFinalConfig(adjustableBaseConfig, baseConfig);
setBaseConfig(newBaseConfig);
}

return {
finalConfigQuery,
updateBaseConfig
}
};
const useFinalConfig = (baseConfig: BaseConfig) => {
const [baseConfig, setBaseConfig] = useState(baseConfig);

// treating the final config as a deriviation of the base config here, I'm sure this isn't right in your code
const finalConfigQuery = useQuery({
queryKey: ['final-config', baseConfig],
queryFn: () => {
return buildFinalConfig(config);
}
});

const updateBaseConfig = (adjustableBaseConfig: AdjustableConfig) => {
const newBaseConfig = mapAdjustableConffigToFinalConfig(adjustableBaseConfig, baseConfig);
setBaseConfig(newBaseConfig);
}

return {
finalConfigQuery,
updateBaseConfig
}
};
extended-salmon
extended-salmonOP9mo ago
finalConfig is built on server. I was thinking about deriving finalConfig from baseConfig using useQuery but faced one issue. I only need to do work on server if important options change, otherwise I should return base config. So I need to have previous version of finalConfig in useQuery which is esentially previously returned data from useQuery. And as queryKey changes due to the fact that baseConfig changes there will be no previous value. I am not sure if there is possibility to get previously returned data from hook no matter what queryKey was. On the other hand It will be nice to have values cached as well. I guess I can call queryClient.getQueryData from mutationFn , if not data available then proceed with API cal and in onSuccess store result into cache too using queryClient.setQueryData.
absent-sapphire
absent-sapphire9mo ago
Could you separate the values that might change from the values that will always need server work?
extended-salmon
extended-salmonOP9mo ago
In the end I need one global config object, so I would need to join it back again if I split it. Anyway, I think I will stick with mutation for now :). Thanks

Did you find this page helpful?