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

relay-query-wrapper

v1.1.0

Published

React+Relay tool to easily render optional components based on query output

Downloads

8

Readme

relay-query-wrapper

npm version npm version

A simple higher-order component that makes it easy to create complex Relay component trees. This makes it easy to:

  • Present granular loading states
  • Optionally render subcomponents based on the presence of data
  • Share query data between with two components that are far apart in the component tree

This functionality is accomplished through the withQuery function.

Examples

All examples use the Star Wars GraphQL schema. For plain JavaScript usage, simply exclude the typey parts.

Basic Example

The main idea behind this wrapper is that you never had to handwrite a usePreloadedQuery or useLazyLoadQuery component yourself. Your components all just consume fragments, then the top-level component in the tree is wrapped in withQuery. You do however still need useQueryLoader somewhere to trigger the query loading.

// FilmList.tsx
interface Props {
  filmsConnectionKey: FilmList_filmsConnection$key;
}

const FilmList: React.FC<Props> = ({ filmsConnectionKey }) => {
  // This component just consumes a fragment key
  const filmsConnection = useFragment(
    graphql`
      fragment FilmList_filmsConnection on FilmsConnection {
        films {
          id
          title
        }
      }
    `,
    filmsConnectionKey
  );

  return (
    <ul>
      {filmsConnection.films.map((film) => (
        <li key={film.id}>{film.title}</li>
      ))}
    </ul>
  );
};

// We have to tell withQuery what prop (in this case filmsConnectionKey) we expect it to auto-populate
export default withQuery<FilmListQuery, Props, "filmsConnectionKey">({
  // The query that will populate this
  query: graphql`
    query FilmListQuery {
      allFilms {
        ...FilmList_filmsConnection
      }
    }
  `,
  // This maps the returned query data into props for your component
  dataToProps: (data) => data.allFilms && { filmsConnectionKey: data.allFilms },
  // Rendered *while the query is loading*
  fallbackElement: <span>Loading...</span>,
})(FilmList);
// HomePage.tsx
import FilmList from "./FilmList";
import type { FilmListQuery as FilmListQueryType } from "./__generated__/FilmListQuery.graphql";
import FilmListQuery from "./__generated__/FilmListQuery.graphql";

const HomePage: React.FC = () => {
  const [queryRef, loadQuery] =
    useQueryLoader<FilmListQueryType>(FilmListQuery);

  // Load data on first render. Warning: this creates a render-then-fetch
  // pattern which is discouraged in Relay, but this is just an example
  useEffect(() => {
    loadQuery({});
  }, [loadQuery]);

  return <FilmList queryRef={queryRef} />;
};

Passthrough Props

Your wrapped component can accept props other than your query data:

// FilmList.tsx
interface Props {
  filmsConnectionKey: FilmList_filmsConnection$key;
  color: string;
}

const FilmList: React.FC<Props> = ({ filmsConnectionKey, color }) => {
  // Component contents are the same as the basic example, but you can access
  // the color prop now
};

export default withQuery<FilmListQuery, Props, "filmsConnectionKey">({
  // This still only needs to populate the Relay key. All other props are pass
  // through transparently.
  dataToProps: (data) => data.allFilms && { filmsConnectionKey: data.allFilms },
  // Omitting other fields from the basic example...
})(FilmList);
// HomePage.tsx
const HomePage: React.FC = () => {
  // Omitting useQueryLoader from the basic example...
  return <FilmList queryRef={queryRef} color="red" />;
};

Multiple Consumers

You can easily have multiple disjoint components consume the same query, simply by using multiple instances of withQuery. You need to give them a common query definition though.

Note: This example is substantially simplified, in practice you would simply use a fragment with a common parent for this use case. But in sufficiently complex UIs, you may need two disjoint query consumers, in which case this pattern comes in handy.

// queries.ts

// The query name has to start with the file name, hence `queriesFilmListQuery`
const filmListQuery = graphql`
  query queriesFilmListQuery {
    allFilms {
      ...FilmList_filmsConnection
    }
  }
`;
// FilmList.tsx
// Omitting component definition from the basic example...

export default withQuery<queriesProblemQuery, Props, "filmsConnectionKey">({
  // The query that will populate this
  query: filmListQuery,
  // Omitting other fields from the basic example...
})(FilmList);
// FilmList2.tsx

// Imagine another consumer of the same query data, identical to FilmList.tsx
// HomePage.tsx
import FilmList from "./FilmList";
import FilmList2 from "./FilmList2";
// Notice we import the generated query still, *not* the value directly from
// queries.ts
import type { queriesFilmListQuery as queriesFilmListQueryType } from "./__generated__/queriesFilmListQuery.graphql";
import queriesFilmListQuery from "./__generated__/queriesFilmListQuery.graphql";

const HomePage: React.FC = () => {
  // We'll use this query ref for both film lists, meaning we only have to make
  // the query once.
  const [queryRef, loadQuery] =
    useQueryLoader<queriesFilmListQueryType>(queriesFilmListQuery);

  // Load data on first render. Warning: this creates a render-then-fetch
  // pattern which is discouraged in Relay, but this is just an example
  useEffect(() => {
    loadQuery({});
  }, [loadQuery]);

  return (
    <div>
      <FilmList queryRef={queryRef} />
      <FilmList2 queryRef={queryRef} />
    </div>
  );
};

Optional Rendering

withQuery lets you easily render a query-consuming component only under certain circumstances. For example, if you have a search bar and only want to render data once a film ID is entered:

// FilmDetail.tsx
interface Props {
  filmKey: FilmDetail_film$key;
}

const FilmDetail: React.FC<Props> = ({ filmKey }) => {
  const film = useFragment(
    graphql`
      fragment FilmDetail_film on Film {
        title
      }
    `,
    filmKey
  );

  return <div>The name of this film is: {film.title}</div>;
};

export default withQuery<FilmListQuery, Props, "filmKey">({
  query: graphql`
    query FilmQuery($filmID: ID!) {
      film(filmID: $filmID) {
        ...FilmDetail_film
      }
    }
  `,
  dataToProps: (data) => data.film && { filmKey: data.film },
  fallbackElement: <span>Loading...</span>,
  // Rendered when no search term is entered, i.e. before query starts
  preloadElement: <span>Search for a film</span>,
  // Rendered if the query comes up empty, i.e. dataToProps returns null
  noDataElement: <span>Film not found!</span>,
})(FilmList);
// HomePage.tsx
import FilmDetail from "./FilmDetail";
import type { FilmQuery as FilmQueryType } from "./__generated__/FilmQuery.graphql";
import FilmQuery from "./__generated__/FilmQuery.graphql";

const HomePage: React.FC = () => {
  const [filmId, setFilmID] = useState("");

  // We'll use this query ref for both film lists, meaning we only have to make
  // the query once.
  const [queryRef, loadQuery, disposeQuery] =
    useQueryLoader<FilmQueryType>(FilmQuery);

  // Reload the query
  useEffect(() => {
    if (filmID) {
      loadQuery({ filmID });
    } else {
      // If the search bar is cleared out, we want to stop showing results
      disposeQuery();
    }
  }, [loadQuery, filmID]);

  // queryRef will be null before the first fetch. The wrapped FilmDetail will
  // automatically render a predefined placeholder until the user searches
  return (
    <div>
      <SearchBar filmID={filmID} setFilmID={setFilmID} />
      <FilmDetail queryRef={queryRef} />
    </div>
  );
};

Loading Query Ref from Context

Occasionally, you want to store a Relay query ref in context rather than passing it as a prop. You can easily create a consumer of this using withContextQuery:

// FilmListQueryContext.ts
const FilmListQueryContext = React.createContext<{
  // The query ref *has* to be stored under this key!
  queryRef: PreloadedQuery<FilmListQuery> | null | undefined;
}>({});
import FilmListQueryContext from "./FilmListQueryContext";

// FilmList.tsx
interface Props {
  filmsConnectionKey: FilmList_filmsConnection$key;
}

const FilmList: React.FC<Props> = ({ filmsConnectionKey }) => {
  // This component just consumes a fragment key
  const filmsConnection = useFragment(
    graphql`
      fragment FilmList_filmsConnection on FilmsConnection {
        films {
          id
          title
        }
      }
    `,
    filmsConnectionKey
  );

  return (
    <ul>
      {filmsConnection.films.map((film) => (
        <li key={film.id}>{film.title}</li>
      ))}
    </ul>
  );
};

export default withContextQuery<FilmListQuery, Props>({
  // We'll load the queryRef from this context, instead of a prop
  context: FilmListQueryContext,
  query: graphql`
    query FilmListQuery {
      allFilms {
        ...FilmList_filmsConnection
      }
    }
  `,
  dataToProps: (data) => data.allFilms && { filmsConnectionKey: data.allFilms },
  fallbackElement: <span>Loading...</span>,
})(FilmList);
// HomePage.tsx
import FilmList from "./FilmList";
import FilmListQueryContext from "./FilmListQueryContext";
import type { FilmListQuery as FilmListQueryType } from "./__generated__/FilmListQuery.graphql";
import FilmListQuery from "./__generated__/FilmListQuery.graphql";

const HomePage: React.FC = () => {
  const [queryRef, loadQuery] =
    useQueryLoader<FilmListQueryType>(FilmListQuery);

  useEffect(() => {
    loadQuery({});
  }, [loadQuery]);

  return (
    <FilmListQueryContext.Provider value={{ queryRef }}>
      <FilmList />
    </FilmListQueryContext.Provider>
  );
};