DEV Community

Cover image for React Query Mutations Offline React-Native
Christophe Kwizera
Christophe Kwizera

Posted on

React Query Mutations Offline React-Native

Introduction

We are addressing a React-Query mutation issue related to offline functionality. Upon analysis, several discrepancies were identified within the setup. The following points elucidate the observed issues:

Paused & Pending

  1. In React-Query version 5, the useMutation hook returns isPending instead of isLoading. Initially, I misconceived isPending solely as indicative of submission loading. However, it signifies both loading states: when online (isLoading) and when offline (isPending). This is evident by assessing isPaused within useMutation, which reflects false when online and true when offline.

isPaused indicates that the request has been cached and will resume (if configured accordingly) upon device reconnection to the internet.

const {
    reset,
    mutate,
    isPaused,
    isPending,
  } = useCreate();

useEffect(() => {
    if (isPaused) {
      // If a request is pending due to offline work, it may be necessary to reset the form to allow the action to be attempted multiple times despite the offline status.
      reset();
    }
  }, [isMutationPaused]);
Enter fullscreen mode Exit fullscreen mode

Storing (updating) Offline Data

Using react query eliminated the need of redux to data it already hold, you will store the data onMutate and here's how it done.

Remember the queryKey you assigned to the data e.g ['todos']

const useCreate = () => {
  const queryClient = useQueryClient();
  return useMutation({
    onMutate: async variables => {
      // Cancel current queries for the order list
      await queryClient.cancelQueries({ queryKey: ['todos] })

      // Create optimistic todos
      const optimisticTodo = { ...variables };

      // Add optimistic todo to other todos list
      queryClient.setQueryData(
        ['todos'],
        (previousData) => {
          return [...previousData, optimisticTodo ];
        },
      );

      // Return context with the optimistic todo
      return { optimisticTodo };
    },
    mutationFn: (data) => createOrder(data),
  });
};
Enter fullscreen mode Exit fullscreen mode

for more info read about Mutations on React_Query_Docs

Resume Paused Mutations

In my scenario, it appeared that none of the mutations were being paused. Here is the resolution I implemented:

  1. To try to resume paused mutations only if online otherwise it will also throw Network Error and mutation will no longer be paused.
<PersistQueryClientProvider
            client={queryClient}
            persistOptions={{
                persister: syncStoragePersister
            }}
            onSuccess={() => {
                NetInfo.fetch().then(state => {
                    if (state.isConnected) {
                        queryClient.resumePausedMutations()
                    }
                })
            }}
        >
Enter fullscreen mode Exit fullscreen mode
  1. Setting focus manager also only if device is only otherwise this will also try to trigger mutations and if offline they will fail with NetworkError so no longer paused:
NetInfo.fetch().then(state => {
        if (state.isConnected && Platform.OS !== 'web') {
            focusManager.setFocused(status === 'active')
        }
    })
Enter fullscreen mode Exit fullscreen mode

Honorable mention

If you persist offline mutations with the persistQueryClient plugin, mutations cannot be resumed when the page is reloaded unless you provide a default mutation function.

This is a technical limitation. When persisting to an external storage, only the state of mutations is persisted, as functions cannot be serialized. After hydration, the component that triggers the mutation might not be mounted, so calling resumePausedMutations might yield an error: No mutationFn found.

const persister = createSyncStoragePersister({
  storage: window.localStorage,
})
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      gcTime: 1000 * 60 * 60 * 24, // 24 hours
    },
  },
})

// we need a default mutation function so that paused mutations can resume after a page reload
queryClient.setMutationDefaults(['todos'], {
  mutationFn: ({ id, data }) => {
    return api.updateTodo(id, data)
  },
})

export default function App() {
  return (
    <PersistQueryClientProvider
      client={queryClient}
      persistOptions={{ persister }}
      onSuccess={() => {
        // resume mutations after initial restore from localStorage was successful
        queryClient.resumePausedMutations()
      }}
    >
      <RestOfTheApp />
    </PersistQueryClientProvider>
  )
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

I trust you found this information beneficial. For further details, please refer to the conversation on this GitHub issue here

Top comments (0)