Astro Theme: Minimal Blog

repository·main·Indexed 42 days ago

https://github.com/tkdodo/blog

A lightweight, feature-rich blog starter template built with Astro, Tailwind CSS, and MDX. It includes a CLI assistant for scaffolding posts, support for dark mode, search, and specialized MDX components such as <Playground> for live coding, <Tweet />, <Sponsors />, and <Comments />.

Tokens
97K
Snippets
277
Records
330
Agent score
52%

What's inside Astro Theme: Minimal Blog

  1. What is `select` in React Query?

    main

    The select option in useQuery (and useSuspenseQuery) allows you to pick, transform, or compute derived state from the data returned by your queryFn.

    Instead of subscribing a component to the entire query result, select enables fine-grained subscriptions. A component using select will only re-render if the specific value returned by the select function changes. This is highly effective for optimizing performance when an endpoint returns large objects where only a few fields are needed by a specific component, or when some fields change frequently while others change rarely.

    // Example: Selecting only the title to prevent re-renders when other product fields change
    function ProductTitle({ id }: Props) {
      const productTitleQuery = useSuspenseQuery({
        ...productOptions(id),
        select: (data) => data.title,
      })
    
      return <h1>{productTitleQuery.data}</h1>
    }
  2. The pitfalls of using `UseQueryOptions` in custom hooks

    main

    When creating a custom hook that accepts options, using Partial<UseQueryOptions> from @tanstack/react-query often results in the data type being widened to unknown.

    This happens because UseQueryOptions relies on four generics for type inference. If these generics are omitted in your abstraction, they default to unknown. When you spread these options into useQuery, the library's ability to infer the correct data type is lost.

    import { useQuery, type UseQueryOptions } from '@tanstack/react-query'
    type Invoice = { id: number }
    declare function fetchInvoice(id: number): Promise<Invoice>
    
    // This causes 'data' to be 'unknown' because generics are omitted
    function useInvoice(id: number, options?: Partial<UseQueryOptions>) {
      return useQuery({
        queryKey: ['invoice', id],
        queryFn: () => fetchInvoice(id),
        ...options,
      })
    }
    
    const { data } = useInvoice(1, { throwOnError: true })
    //      ^? unknown
  3. Keep background updates active in forms using derived state

    main

    In collaborative environments or for large forms, you may want to keep background updates active. Instead of copying all server state into the form, you can keep the Server State in React Query and only track user changes in the Client State.

    The UI displays derived state: if a field has a value in the form state (Client State), show it; otherwise, fall back to the value from the server (Server State).

    Requirements & Caveats:

    • Requires Controlled Fields: This pattern typically requires using controlled components (e.g., using Controller from react-hook-form) so you can manually manage the value prop.
    • Complexity with Nested Objects: While easy with shallow forms using nullish coalescing (??), merging nested objects can be difficult.
    • UX Considerations: Automatically changing values in the background might confuse users. A better UX might be to highlight fields that are out of sync with the server.
    // Pattern: Derive state from both Client and Server state
    function PersonDetail({ id }) {
      const { data } = useQuery({
        queryKey: ['person', id],
        queryFn: () => fetchPerson(id),
      })
      const { control, handleSubmit } = useForm()
      const { mutate } = useMutation({
        mutationFn: (values) => updatePerson(values),
      })
    
      if (data) {
        return (
          <form onSubmit={handleSubmit(mutate)}>
            <Controller
              name="firstName"
              control={control}
              render={({ field }) => (
                <input
                  {...field}
                  // ✅ derive state: use field value if present, else fallback to server data
                  value={field.value ?? data.firstName}
                />
              )}
            />
            <input type="submit" />
          </form>
        )
      }
    
      return 'loading...'
    }
  4. Understand the risks of index signatures in TypeScript

    main

    When using an index signature (e.g., Record<string, T> or {[key: string]: T}), TypeScript assumes that any access via a key will return a value of type T. However, at runtime, accessing a non-existent key returns undefined. This mismatch can lead to runtime errors (like Cannot read property '...' of undefined) because TypeScript does not force you to check for undefined during index access.

    type Widget = {
      id: string
      title: string
    }
    
    // Index signature: TypeScript assumes any string key returns a Widget
    type WidgetIndex = Record<string, Widget>
    
    const widgetIndex: WidgetIndex = {
      widget1: { id: 'widget1', title: 'Foo' },
    }
    
    // UNSAFE: This will crash at runtime if 'helloWorld' is missing,
    // but TypeScript allows it without warning.
    widgetIndex['helloWorld'].title.toUpperCase()
  5. Understand React Query's data synchronization model

    main

    React Query is a data synchronization tool rather than a simple state manager. It treats server state as something the frontend does not 'own', but rather views as a 'snapshot' of the backend.

    To keep the UI accurate, React Query uses a Stale-While-Revalidate caching mechanism:

    1. It provides cached data immediately (even if it is stale) to avoid showing loading spinners.
    2. It performs a background refetch to revalidate the data against the server.

    This approach prioritizes showing something to the user over showing nothing, improving perceived performance.

  6. Apply the Single Responsibility Principle to useEffect

    main

    Each useEffect should perform one specific task. Avoid bundling multiple unrelated operations into a single effect. This prevents bugs when dependencies change. For example, if you have one effect that both updates the document.title and tracks a page visit, changing the title will trigger the analytics tracking unnecessarily. Splitting them into two separate effects ensures each runs only when its specific dependencies change.

    // BAD: One effect, two responsibilities
    React.useEffect(() => {
      document.title = 'hello world'
      trackPageVisit()
    }, [])
    
    // GOOD: Single responsibility
    const [title, setTitle] = React.useState('hello world')
    
    React.useEffect(() => {
      document.title = title
    }, [title])
    
    React.useEffect(() => {
      trackPageVisit()
    }, [])
  7. Measure DOM nodes using ref callbacks and primitive state

    main

    You can use a ref callback to measure a DOM node and store its dimensions in React state. When storing these values, prefer primitive values (like numbers) over objects.

    If you store a new object (like a DOMRect) in state on every measurement, it will trigger an infinite re-render loop because the object reference changes every time. If you store a primitive (like height), React's useState optimization will bail out of re-rendering when the value remains the same, preventing a loop.

    function MeasureExample() {
      const [height, setHeight] = React.useState(0)
    
      const measuredRef = React.useCallback((node) => {
        if (node !== null) {
          setHeight(node.getBoundingClientRect().height)
        }
      }, [])
    
      return (
        <>
          <h1 ref={measuredRef}>Hello, world</h1>
          <h2>The above header is {Math.round(height)}px tall</h2>
        </>
      )
    }
  8. How Tracked Queries work

    main

    Tracked Queries allow React Query to automatically detect which properties of the query object are being used during render and only trigger re-renders when those specific properties change. This removes the need to manually maintain a notifyOnChangeProps array.

    To enable this, set notifyOnChangeProps: 'tracked'. You can set this globally in the QueryClient or per-query.

    Important Limitations:

    • Object Rest Destructuring: Using const { isLoading, ...queryInfo } = useQuery(...) will cause the observer to track all fields because queryInfo contains everything. Use explicit destructuring instead: const { isLoading, data } = useQuery(...).
    • Effect Tracking: Fields accessed only inside useEffect are not tracked. To ensure a field is tracked, it must be accessed during the render phase (e.g., by including it in the useEffect dependency array).
    • No Reset: Once a field is tracked during a render cycle, it remains tracked for the lifetime of the observer, even if it is no longer used in subsequent renders (e.g., inside a conditional block).
    // Global configuration for tracked queries
    const queryClient = new QueryClient({
      defaultOptions: {
        queries: {
          notifyOnChangeProps: 'tracked',
        },
      },
    })
    
    function App() {
      return (
        <QueryClientProvider client={queryClient}>
          <Example />
        </QueryClientProvider>
      )
    }
  9. Use Object Query Keys for better destructuring and fuzzy matching

    main

    Instead of using purely positional array elements in your query keys, you can use objects within the top-level array. This allows for named destructuring in your queryFn, which is more resilient to changes in the key hierarchy than positional array destructuring (e.g., const [, , state] = queryKey).

    Because TanStack Query uses fuzzy matching (via partialDeepEqual), object keys work seamlessly for invalidation. A key advantage of objects is that they are unordered, allowing you to perform fuzzy matching on specific properties regardless of their position in the object, enabling powerful cross-scope invalidations.

    // Define a key factory using objects inside arrays
    const todoKeys = {
      all: [{ scope: 'todos' }] as const,
      lists: () => [{ ...todoKeys.all[0], entity: 'list' }] as const,
      list: (state: State, sorting: Sorting) =>
        [{ ...todoKeys.lists()[0], state, sorting }] as const,
    }
    
    // Use named destructuring in the query function
    const fetchTodos = async ({
      queryKey: [{ state, sorting }],
    }: QueryFunctionContext<ReturnType<(typeof todoKeys)['list']>>) => {
      const response = await axios.get(`todos/${state}?sorting=${sorting}`)
      return response.data
    }
    
    // Usage in a hook
    export const useTodos = () => {
      const { state, sorting } = useTodoParams()
      return useQuery({
        queryKey: todoKeys.list(state, sorting),
        queryFn: fetchTodos,
      })
    }
  10. Understand closures and stale closures in React

    main

    A closure is a JavaScript concept where a function 'sees' and interacts with variables defined in its outer scope. In React, functions created during a render (like event handlers or those inside hooks) capture the variables available at that specific moment in time.

    The 'Taking a Picture' Analogy: Think of creating a function as taking a photograph. The photo captures the function's code (foreground) and the surrounding variables (background). Once the photo is taken, the background is frozen.

    • Normal Renders: When a component re-renders, React re-runs the function, effectively taking a new picture with the latest variable values.
    • Stale Closures: If you prevent a function from being re-created (e.g., by using an empty dependency array in useCallback or useMemo), you are forced to look at an old photograph. This old photo contains the variable values from the moment the function was first created, which may no longer match the current state of the application. This is known as a stale closure.
    function Counter({ count }) {
      const logCount = () => {
        // 💡 accessing count from the outer scope
        console.log('count', count)
      }
    
      return <button onClick={logCount}>Show me the count</button>
    }