React:查询,延迟useReducer

React: Query, delay useReducer

我已经为这个问题绞尽脑汁好一段时间了,但我仍然不确定该怎么做。

基本上,我通过 useQuery 从数据库中提取数据,这一切都很好而且很漂亮,但我也在尝试使用 useReducer(我仍然不是 100% 熟悉)将 initial data 保存为 state 以检测是否进行了任何更改。

问题:

当useQuery忙于取数据时,initial dataundefined;这就是保存为 state 的内容。这会导致验证和保存等方面的各种问题。

这是我的主要表单函数:

function UserAccountDataForm({userId}) {
    const { query: {data: userData, isLoading: isLoadingUserData} } = useUserData(userId);
    
    const rows = React.useMemo(() => {
        if (userData) { /* Process userData here into arrays */ }
        return [];
    }, [isLoadingUserData, userData]); // Watches for changes in these values

    const { isDirty, methods } = useUserDataForm(handleSubmit, userData);
    const { submit, /* updateFunctions here */ } = methods;

    if (isLoadingUserData) { return <AccordionSkeleton /> } // Tried putting this above useUserDataForm, causes issues

    return (
        <>
            Render stuff here
            *isDirty* is used to detect if changes have been made, and enables "Update Button"
        </>
    )
}

这里是useUserData(负责从DB中拉取数据):

export function useUserData(user_id, column = "data") {
    const query = useQuery({
        queryKey: ["user_data", user_id],
        queryFn: () => getUserData(user_id, column), // calls async function for getting stuff from DB
        staleTime: Infinity,
    });
}

return { query }

这里是减速器:

function userDataFormReducer(state, action) {
    switch(action.type) {
        case "currency":
            return {... state, currency: action.currency} 
// returns data in the same format as initial data, with updated currency. Of course if state is undefined, formatting all goes to heck
        default:
            return;
    }
}

function useUserDataForm(handleSubmit, userData) {
    const [state, dispatch] = React.useReducer(userDataFormReducer, userData);
    console.log(state) // Sometimes this returns the data as passed; most of the times, it's undefined.
    const isDirty = JSON.stringify(userData) !== JSON.stringify(state); // Which means this is almost always true.

    const updateFunction = (type, value) => { // sample only
        dispatch({type: type, value: value});
    }
}

export { useUserDataForm };

使问题复杂化的是它并不总是会发生。主窗体位于 <Tab>;如果用户切换进出选项卡,有时 state 会在其中包含正确的 initial data,并且一切都按预期工作。

我能想到的最快的“修复”是在 useQuery 为 运行 时不设置 initial data(通过不调用 reducer)。不幸的是,我不确定这是 possible。还有什么我可以尝试解决这个问题的吗?

Compounding the issue is that it doesn't always happen. The main form resides in a ; if the user switches in and out of the tab, sometimes the state will have the proper initial data in it, and everything works as expected.

这很可能是预料之中的,因为 useQuery 会从缓存中返回数据(如果有的话)。因此,如果您返回到您的选项卡,useQuery 将已经有数据并且只进行后台重新获取。由于useReducer是在组件挂载时启动的,所以在这些场景下可以获取服务器数据

有两种方法可以解决这个问题:

  1. 拆分执行查询的组件和具有本地状态的组件 (useReducer)。然后,您可以决定只在查询已经有数据时挂载具有 useReducer 的组件。请注意,如果你这样做,你基本上 选择退出 反应查询的所有很酷的后台更新功能:任何可能产生新数据的额外提取都不会 被“复制”过来。这就是为什么我建议如果你这样做,你关闭查询以避免不必要的提取。简单示例:
const App = () => {
 const { data } = useQuery(key, fn)

 if (!data) return 'Loading...'
 
 return <Form data={data} />

}

const Form = ({ data }) => {
  const [state, dispatch] = useReducer(userDataFormReducer, data)

}

由于减速器仅在数据可用时挂载,因此您不会遇到该问题。

  1. 不要在任何地方复制服务器状态 :) 我更喜欢这种方法,因为它使服务器和客户端状态分开,并且与 useReducer 配合得很好。这是一个关于如何实现该目标的示例 from my blog
const reducer = (amount) => (state, action) => {
  switch (action) {
    case 'increment':
      return state + amount
    case 'decrement':
      return state - amount
  }
}

const useCounterState = () => {
  const { data } = useQuery(['amount'], fetchAmount)
  return React.useReducer(reducer(data ?? 1), 0)
}

function App() {
  const [count, dispatch] = useCounterState()

  return (
    <div>
      Count: {count}
      <button onClick={() => dispatch('increment')}>Increment</button>
      <button onClick={() => dispatch('decrement')}>Decrement</button>
    </div>
  )
}

是否可行完全取决于您的减速器试图实现的目标,但它可能看起来像这样:

const App = () => {
 const { data } = useQuery(key, fn)
 const [state, dispatch] = useReducer(userDataFormReducer)
 const currency = state.currency ?? data.currency
}

通过分开保存服务器状态和客户端状态,您将只存储用户选择的内容。像货币这样的“默认值”不在状态中,因为它本质上是状态复制。如果货币未定义,由于 ?? 运算符,您仍然可以选择显示服务器状态。

另一个优点是脏检查相对容易(我的客户端状态是未定义的吗?)重置为初始状态也只是将客户端状态设置回未定义。

所以实际状态本质上是根据您从服务器获得的内容和用户输入的内容计算出的状态,当然优先考虑用户输入。