npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

normalized-react-query

v1.0.1

Published

Wrapper around React Query to enforce type-safe, consistent key-query mappings.

Downloads

34

Readme

npm package License Quality

Contents

Introduction

React Query provides powerful API state management, with caching, pre-fetching, SSR, and hook support.

The main idea is pairing "keys" (unique to a specific API call + params) with a query function. React Query handles actual pairing of async logic with synchronous hooks/renders behind the scenes.

The "problem" with manual pairing of keys to functions is that there is no way to ensure the exact same function is paired to the same key, or that the same key is re-used for similar queries. As a result, either caching may prevent the desired function from being called, or may accidentally call the same function multiple times.

Take these example hooks, which represent inconsistent usage of keys + query handlers:

import { useQuery } from '@tanstack/react-query';
import { getUsers } from './api/users.js';

const useExample = () => {
    const firstQuery = useQuery(
        ['users', 'get'],
        async () => {
            return getPosts();
        };
    );

    const secondQuery = useQuery(
        ['users', 'get'],
        async () => {
            // NEVER RUNS
            // Uses cached value of `firstQuery`.
            // Result is typed to include `{ decorate: boolean }` but that will never exist.
            const users = await getUsers();
            return users.map(user => {
                ...user,
                decorate: true,
            };
        };
    );

    const thirdQuery = useQuery(
        // Different key, same API call.
        // Triggers another fetch for data that already exists.
        ['users', 'fetch'],
        async () => {
            return getUsers();
        };
    );
};

This package attempts to solve this discrepancy by forcing pairing of keys and functions, with strongly typed data.

Ideally any implementations of the wrapper classes can offload any API and business logic so that actual React functions can simply access the data as it becomes available.

Install

npm i normalized-react-query

Example

import type { QueryKey } from '@tanstack/react-query';
import { useState } from 'react';
import {
    Paginated,
    type QueryData,
    type QueryParams,
    Resource,
} from 'normalized-react-query';
import { getUsers, listUsers } from './api/users.js';

class FetchUser extends Resource<User, UserId> {
    protected getKey(id: QueryParams<this>): QueryKey {
        return ['users', 'get', id];
    }
    protected async queryFn(id: QueryParams<this>): Promise<QueryData<this>> {
        return getUser(id)
    }
}
const fetchUser = new FetchUser();

class ListUsers extends Paginated<User> {
    protected getKey(): QueryKey {
        return ['posts', 'get'];
    }
    protected async queryFn(): Promise<QueryData<this>> {
        return listUsers();
    }
    protected async onSuccess(
        client: QueryClient,
        params: QueryParams<this>,
        data: QueryData<this>
    ): void {
        for (const user of data) {
            // Pre-populate lookup data of users by-id.
            fetchUser.setData(client, user.id, user);
        }
    }
}
const listUsers = new ListUsers();

const useExample = () => {
    const firstQuery = fetchUser.useQuery(123);
    // Successfully cached
    const secondQuery = fetchUser.useQuery(123);
    // Separate query, consistent behavior
    const thirdQuery = fetchUser.useQuery(456);

    // Pre-populates every user
    const listQuery = listUsers.useQuery();

    const [showFourth, setShowFourth] = useState(false);
    useEffect(() => {
        setTimeout(() => setShowFourth(true), 5000);
    });

    // Will be cached immediately on load, due to "listUsers" pre-population.
    const fourthQuery = fetchUser.useQuery(789, {
        // All normal React-Query options are available
        enabled: showFourth,
    });
};

Usage

normalized-react-query is an ESM module. That means it must be imported. To load from a CJS module, use dynamic import const { Resource } = await import('normalized-react-query');.

Both the react and react query modules are required for this package to work. Peer dependencies are declared on both. This package merely enforces typing and structure, any caching/revalidation/subscription is still implemented by native React Query.

Class interfaces are used to best expose inheritance and override functionality for typescript. The actual query instances used should be singletons (e.g. create once and re-use).

A lowercase version of each class is available in favor of functional programming practices.

e.g.

import { resource, Resource } from 'normalized-react-query';
import { getUser, type User } from './api.js';

class GetUserClass extends Resource<User, string> {
    getKey(id) {
        return ['users', id];
    }
    queryFn(id) {
        return getUser(id);
    }
    onError(client, id, error) {
        console.error(`Failed to lookup user ${id}`, error);
    }
}
const getUserClass = new GetUserClass();

// Logically same as `getUserClass`.
const getUser = resource<User, string>(
    {
        getKey(id) {
            return ['users', id];
        }
        queryFn(id) {
            return getUser(id);
        }
    },
    {
        onError(client, id, error) {
            console.error(`Failed to lookup user ${id}`, error);
        }
    }
);

API

Resource

The Resource class is the most basic wrapper around useQuery. When in doubt, a method that asynchronously loads data (a "resource") should extend the Resource class.

Each child class defines the parameters to the function, and generates a unique queryKey for those params. It also provides some basic abstractions to support other React Query functionality such as pre-fetching data and invalidation.

The query function can be as simple as a direct API call, but supports side effects as necessary. A common side effect may be to take a sub-resource of the response, and pre-load another query.

These side-effects should most likely be placed in lifecycle hooks, like onSuccess.

Paginated

The Paginated class is a typed extension Resource. It provides some default typing to support pagination, which React Query supports natively with keepPreviousData.

Infinite

The Infinite class wraps the Paginated class further, providing "infinite" queries where all pages are loaded and available in parallel. It is built off multiple individual queries, so any refetching, caching, invalidation, and de-duplication is handled smoothy.

Why not native useInfinite?

React Query exports a useInfinite natively, which provides very similar behavior out of the box. The decision to not use it is based on that hook conflicting with useQuery's cache. useInfinite queries cannot share a queryKey with useQuery, and therefore cannot benefit from the powerful functionality React Query defines.

Therefore, a custom useInfinite hook was implemented using React Query's useQueries hook. That ensures any using of Infinites useQuery hook can benefit from useInfinite and vice versa. Remember Infinite is a child class of Paginated and therefore supports normal query behavior as well.

Mutation

The Mutation class is a wrapper around React Query useMutation. "Mutations" aren't fetching data, but rather changing the server state, and handling side effects accordingly.

Most common mutations are POST and PATCH endpoints, that most likely impact a related GET endpoint. Therefore the "side effects" should either set the new state data (if returned from the mutation) or invalidate the existing query and force it to refetch.

Mutations are not "cached" in the same sense useQuery is, but do generally benefit from the pattern of consistent mutation handling and typings.

Types

A few types are exported for convenience of accessing the generic parameters of a resource.

All accept the this instance of a child class.

QueryData

Access the returned data type of useQuery.

QueryParams

Access the parameter data type to useQuery.

QueryVariables

Unique to Mutation, access the variables interface provided to mutation execution.

EmptyObject

Represents {} type, with no keys. Can be passed as parameter to Paginated/Infinite generics when there are no other parameters to provide.