Home >Web Front-end >JS Tutorial >Building a Data Table with Optimistic Updates

Building a Data Table with Optimistic Updates

DDD
DDDOriginal
2024-11-04 07:28:011030browse

Introduction

Today, I'll share how I built a polished food database management system using modern React patterns. We'll focus on creating a responsive data table with seamless optimistic updates, combining the power of TanStack Query (formerly React Query) with Mantine's component library.

Project Overview

Requirements

  • Display food items in a data table
  • Add new items with immediate feedback
  • Handle loading and error states gracefully
  • Provide smooth optimistic updates

Tech Stack

  • TanStack Query: Server state management
  • Mantine UI: Component library and form management
  • Mantine React Table: Advanced table functionality
  • Wretch: Clean API calls
  • TypeScript: Type safety

Implementation Guide

1. Setting Up the Foundation

First, let's define our types and API configuration:

// Types
export type GetAllFoods = {
  id: number;
  name: string;
  category: string;
};

export type CreateNewFoodType = Pick<
  GetAllFoods,
  | 'name'
  | 'category'
>;

// API Configuration
export const API = wretch('<http://localhost:9999>').options({
  credentials: 'include',
  mode: 'cors',
  headers: {
    'Content-Type': 'application/json',
    Accept: 'application/json',
  },
});

// TANSTACK QUERY 
export const getFoodOptions = () => {
  return queryOptions({
    queryKey: ['all-foods'],
    queryFn: async () => {
      try {
        return await API.get('/foods')
          .unauthorized(() => {
            console.log('Unauthorized');
          })
          .json<Array<GetAllFoods>>();
      } catch (e) {
        console.log({ e });
        throw e;
      }
    },
  });
};

export const useGetAllFoods = () => {
  return useQuery({
    ...getFoodOptions(),
  });
};

2. Building the Data Table

The table component using Mantine React Table:

const FoodsView = () => {
  const { data } = useGetAllFoods();

  const columns = useMemo<MRT_ColumnDef<GetAllFoods>[]>(
    () => [
      {
        accessorKey: 'id',
        header: 'ID',
      },
      {
        accessorKey: 'name',
        header: 'Name',
      },
      {
        accessorKey: 'category',
        header: 'Category',
      },
      // ... other columns
    ],
    []
  );

  const table = useMantineReactTable({
    columns,
    data: data ?? [],
    // Optimistic update animation
    mantineTableBodyCellProps: ({ row }) => ({
      style: row.original.id < 0 ? {
        animation: 'shimmer-and-pulse 2s infinite',
        background: `linear-gradient(
          110deg,
          transparent 33%,
          rgba(83, 109, 254, 0.2) 50%,
          transparent 67%
        )`,
        backgroundSize: '200% 100%',
        position: 'relative',
      } : undefined,
    }),
  });

  return <MantineReactTable table={table} />;
};

3. Creating the Form

A form component for adding new foods:

const CreateNewFood = () => {
  const { mutate } = useCreateNewFood();

  const formInputs = [
    { name: 'name', type: 'text' },
    { name: 'category', type: 'text' },
  ];

  const form = useForm<CreateNewFoodType>({
    initialValues: {
      name: '',
      category: '',
      // ... other fields
    },
  });

  return (
    <Box mt="md">
      <form onSubmit={form.onSubmit((data) => mutate(data))}>
        <Flex direction="column" gap="xs">
          {formInputs.map((input) => (
            <TextInput
              key={input.name}
              {...form.getInputProps(input.name)}
              label={input.name}
              tt="uppercase"
              type={input.type}
            />
          ))}
          <Button type="submit" mt="md">
            Create New
          </Button>
        </Flex>
      </form>
    </Box>
  );
};

4. Implementing Optimistic Updates

The heart of our implementation - TanStack Query mutation with optimistic updates:

export const useCreateNewFood = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationKey: ['create-new-food'],
    mutationFn: async (data: CreateNewFoodType) => {
      await new Promise(resolve => setTimeout(resolve, 3000)); // Demo delay
      return API.url('/foods').post(data).json<GetAllFoods>();
    },
    onMutate: async (newFood) => {
      // Cancel in-flight queries
      await queryClient.cancelQueries({ queryKey: ['all-foods'] });

      // Snapshot current state
      const previousFoods = queryClient.getQueryData<GetAllFoods[]>(['all-foods']);

      // Create optimistic entry
      const optimisticFood: GetAllFoods = {
        id: -Math.random(),
        ...newFood,
        verified: false,
        createdBy: 0,
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
      };

      // Update cache optimistically
      queryClient.setQueryData(['all-foods'], (old) =>
        old ? [...old, optimisticFood] : [optimisticFood]
      );

      return { previousFoods };
    },
    onError: (err, _, context) => {
      // Rollback on error
      if (context?.previousFoods) {
        queryClient.setQueryData(['all-foods'], context.previousFoods);
      }
    },
    onSettled: () => {
      // Refetch to ensure consistency
      queryClient.invalidateQueries({ queryKey: ['all-foods'] });
    },
  });
};

5. Animation Styles

The animation that brings our optimistic updates to life:

@keyframes shimmer-and-pulse {
  0% {
    background-position: 200% 0;
    transform: scale(1);
    box-shadow: 0 0 0 0 rgba(83, 109, 254, 0.2);
  }
  50% {
    background-position: -200% 0;
    transform: scale(1.02);
    box-shadow: 0 0 0 10px rgba(83, 109, 254, 0);
  }
  100% {
    background-position: 200% 0;
    transform: scale(1);
    box-shadow: 0 0 0 0 rgba(83, 109, 254, 0);
  }
}

Best Practices

  1. Optimistic Updates
    • Immediately update UI for better UX
    • Handle error cases with rollbacks
    • Maintain data consistency with proper invalidation
  2. Type Safety
    • Use TypeScript for better maintainability
    • Define clear interfaces for data structures
    • Leverage type inference where possible
  3. Performance
    • Cancel in-flight queries during updates
    • Use proper query invalidation
    • Implement efficient form state management
  4. User Experience
    • Provide immediate feedback
    • Show loading states
    • Handle errors gracefully

Future Enhancements

Consider these improvements for your implementation:

  • Undo/redo functionality
  • Form validation rules
  • Error boundary implementation

Results

Building a Data Table with Optimistic Updates

Once Completed Request

Building a Data Table with Optimistic Updates

Conclusion

This implementation demonstrates how to create a robust data management system using modern React patterns. The combination of TanStack Query, Mantine UI, and thoughtful optimistic updates creates a smooth and professional user experience.

Remember to:

  • Keep your components focused and maintainable
  • Handle all possible states (loading, error, success)
  • Use TypeScript for better code quality
  • Consider user experience in your implementation

What challenges have you faced implementing optimistic updates in your React applications? Share your experiences in the comments below.

The above is the detailed content of Building a Data Table with Optimistic Updates. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn