Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(frontend): Utilize TanStack Query #5096

Merged
merged 44 commits into from
Nov 22, 2024
Merged

Conversation

amanape
Copy link
Member

@amanape amanape commented Nov 18, 2024

End-user friendly description of the problem this fixes or functionality that this introduces
Maintaining fetch logic, such as caching and handling requests through React Router's data loaders, can introduce complexity and make the code harder for others to understand.

  • Include this change in the Release Notes. If checked, you must provide an end-user friendly description for your change below

Give a summary of what the PR does, explaining any non-trivial design decisions
Use TanStack Query for improved cache management and better handling of client-side request context. Favor React hooks over data loaders.

General Pattern

For queries

A basic implementation of a query would look something like this:

useQuery({
  queryKey: ["files", token, path], // changes in the key will trigger a new request
  queryFn: () => OpenHands.getFiles(token!, path),
  enabled: !!token, // only fetch if this is true
});

You can read more about the basics here: https://tanstack.com/query/latest/docs/framework/react/guides/queries

For better reusability and readability, we wrap it within a custom hook:

export const useGetFiles = (config?: UseListFilesConfig) => {
  const { token } = useAuth();
  const { status } = useWsClient();
  const isActive = status === WsClientProviderStatus.ACTIVE;

  return useQuery({
    queryKey: ["files", token, config?.path], // changes in the key will trigger a new request
    queryFn: () => OpenHands.getFiles(token!, config?.path),
    enabled: isActive && config?.enabled && !!token, // only fetch if the client is active and the token is present
  });
};

Example usage:

const { data: paths } = useGetFiles({
  path,
  enabled: isDirectory && isOpen,
});

For mutations

Similarly, we wrap the mutation within a hook:

export const useSubmitFeedback = () => {
  const { token } = useAuth();

  return useMutation({
    mutationFn: ({ feedback }: SubmitFeedbackArgs) =>
      OpenHands.submitFeedback(token || "", feedback),
    onError: (error) => {
      toast.error(error.message);
    },
  });
};

One thing to note are the available mutation side effects (onError). You can read more about them here: https://tanstack.com/query/latest/docs/framework/react/reference/useMutation

Example usage:

const { mutate: submitFeedback, isPending } = useSubmitFeedback();

submitFeedback(
  { feedback },
  {
    onSuccess: (data) => {
      const { message, feedback_id, password } = data.body; // eslint-disable-line
      const link = `${VIEWER_PAGE}?share_id=${feedback_id}`;
      shareFeedbackToast(message, link, password);
      onClose();
    },
  },
);

The same callback functions are provided to the mutate function, though caution the order of execution. Read more here: https://tanstack.com/query/latest/docs/framework/react/guides/mutations#mutation-side-effects

Caching behaviour

By default, cache time (gcTime) is set to 5 minutes and stale time (staleTime) is set to 0.

gcTime

The time in milliseconds that unused/inactive cache data remains in memory. When a query's cache becomes unused or inactive, that cache data will be garbage collected after this duration. When different garbage collection times are

staleTime

The time in milliseconds after data is considered stale. This value only applies to the hook it is defined on.

Basic example

  1. Query mounts and fetches the data
  2. Stores the data in cache
  3. Becomes stale immediately (default of 0ms)
  4. Query mounts elsewhere
  5. Fetches data from cache; then refetches stale data

You can read more here: https://tanstack.com/query/latest/docs/framework/react/guides/caching

The only case where I changed the defaults is for the useIsAuthed hook (for authenticating):

export const useIsAuthed = () => {
  const { gitHubToken } = useAuth();
  const { data: config } = useConfig();

  const appMode = React.useMemo(() => config?.APP_MODE, [config]);

  return useQuery({
    queryKey: ["user", "authenticated", gitHubToken, appMode],
    queryFn: () => OpenHands.authenticate(gitHubToken || "", appMode!),
    enabled: !!appMode,
    staleTime: 1000 * 60 * 5, // 5 minutes
  });
};

Changelog

  • Utilize useQuery and useMutation hooks in place of regular requests made in data loaders
  • Move away from using custom requests function, useQuery and useMutation already handle error and success cases more elegantly
  • Remove custom caching logic in favor of TanStack Query caching
  • Replace the use of <fetcher.Form> and <Form> that call to action resource routes and handle them with custom submit handlers

We previously relied on loaders and actions to manage state across the app. Since retiring them, we now need to use appropriate hooks and contexts. Additionally, directly accessing localStorage is considered a bad practice because there's no reliable way to "subscribe" to changes. This approach conflicts with React's re-rendering lifecycle, leading to outdated or inconsistent data. Any logic that causes side effects or requires maintaining state should be migrated to a hook or a context to ensure proper synchronization and reactivity.

  • Create an AuthContextProvider accessible via the useAuth hook that maintains token and ghToken states as well as persist them in localStorage
  • Create a UserPrefsProvider accessible via the useUserPrefs hook that handles changes in settings and whether its up-to-date
  • Replace clearSession with a new useEndSession hook that resets some state and navigates back to home

To run this PR locally, use the following command:

docker run -it --rm   -p 3000:3000   -v /var/run/docker.sock:/var/run/docker.sock   --add-host host.docker.internal:host-gateway   -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:cbb6a27-nikolaik   --name openhands-app-cbb6a27   docker.all-hands.dev/all-hands-ai/openhands:cbb6a27

@amanape amanape self-assigned this Nov 18, 2024
@amanape amanape marked this pull request as ready for review November 22, 2024 09:15
@amanape amanape requested review from rbren and tofarr and removed request for rbren November 22, 2024 11:59
Comment on lines +190 to +197
const response = await fetch("/api/submit-feedback", {
method: "POST",
body: JSON.stringify(data),
body: JSON.stringify(feedback),
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use the request helper which does the token plumbing for us? I don't like the idea of passing token around the app--it adds a lot more plumbing

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our current request handler is quite messy. A major concern is how request does error handling. TanStack Query already manages error by catching thrown errors, but the request handler introduces its own error-handling logic. This complicates integration with TanStack Query and also makes custom error handling more difficult.

IMO retrieving the token from local storage would no longer be ideal. Our updated auth context now manages the token state (to address the improper retrieval of tokens via local storage throughout the app), and we use the useAuth context hook within the custom query hooks that call the request function. Basically very similar to what request tried to do but this better follows React principles and keeps the code more consistent and maintainable.

Final note is that request breaks some practice by introducing any types and ignores some eslint rules

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can come back to this if we really want to, just probably create or heavily patch the existing request

Copy link
Collaborator

@rbren rbren left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall LGTM. I'd like @mamoodi do do a small QA run on this branch given how much is changing though

@amanape amanape merged commit becb17f into main Nov 22, 2024
14 checks passed
@amanape amanape deleted the ALL-726/replace-loaders-with-query branch November 22, 2024 19:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants