relay-hooks lets a Relay application use hooks before react-relay had them
Use Relay as React hooks
At a glance
- What is it?
- relay-hooks is a TypeScript library that exposes Relay's query and mutation operations as React hooks, including useQuery, useLazyLoadQuery, useFragment, usePagination and useSubscription, with a RelayEnvironmentProvider that puts the environment in context. It predates the hooks in react-relay and the README tracks the remaining differences between them.
- Who is it for?
- Adopt relay-hooks if you have a Relay application on React 16 or 17 that wants the hook style without moving to react-relay's own hooks, since the library implements the store-or-network and store-only policies and offers useQuery without suspense so you do not have to migrate an application to concurrent mode. Do not adopt it for a new project on a current React, because react-relay ships hooks of its own and the peer dependency range here stops at React 17.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 142 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the library is for, and where it sits in the Relay story
relay-hooks describes itself in two lines: use Relay as React hooks, and Relay Hooks, in the package description.
The history in the README explains why it exists. The initial purpose of the library was to provide the ability to use all react-relay HOCs as react hooks, and to implement the store-or-network and store-only policies used by the react-relay-offline library to manage offline relay applications. So the origin is twofold: a hook-shaped wrapper over the higher-order components, and a faithful implementation of the fetch policies an offline-first Relay application needs.
Then comes the correction. After Relay's core team shared information about the initial differences in issue 5, all the necessary changes were made in order to make relay-hooks as close as possible to their specifications.
That is the key sentence for anyone evaluating it. The library was written against the shape Relay's own hooks would take, and the remaining differences are listed explicitly rather than left to be discovered.
The listed differences are small and specific. useLazyLoadQuery returns a single data object with the query's data, and nothing else. And useFragment is called useSuspenseFragment in relay-hooks.
That second one matters more than it looks. react-relay later adopted the name useFragment, so an application that starts on relay-hooks and later migrates to react-relay's hooks has to rename every call site. The library's own naming is a remnant of the period when the hook was suspense-only.
The topics list names graphql, hooks, react, react-hooks, refetch, relay-hooks, relay-modern, ssr, suspense, and the individual hook names usefragment, usemutation and usequery. The primary language is TypeScript, the licence is MIT, the default branch is master, and the last push was on 2026-06-03. The recent releases are v10.0.2 from that date, v10.0.1 from 2026-05-13 and v10.0.0 from 2025-09-04.
The environment moved into context, which changed every call site
The most consequential design decision in the library is how the Relay environment reaches the hooks, and it is also the one most likely to surprise you.
The README states it plainly: since queries with useQuery no longer set context, we will expose a new RelayEnvironmentProvider component that takes an environment and sets it in context, and variables will no longer be part of context. A RelayEnvironmentProvider should be rendered once at the root of the app, and multiple useQuery's can be rendered under this environment provider.
import { RelayEnvironmentProvider } from 'relay-hooks';
ReactDOM.render(
<RelayEnvironmentProvider environment={modernEnvironment}>
<AppTodo/>
</RelayEnvironmentProvider>,
rootElement,
);The consequence for you is that useQuery does not take an environment as an argument. Instead, it reads the environment set in the context, and this also implies that it does not set any React context. In addition to query as the first argument and variables as the second, useQuery accepts a third argument, options.
So the environment is application-scoped rather than component-scoped. That is the modern React pattern, and it is what makes it possible for many useQuery calls to live under one provider without each one threading an environment through props. The trade is that the environment is not swappable per component, so a test that needs a different environment has to wrap in a different provider rather than pass a prop.
Note also that variables are out of context entirely. They are passed to the hook as the second argument, per component, which is the behaviour react-relay's own hooks also settled on.
The four fetch policies, and the options that change behaviour
The options object for useQuery and useLazyLoadQuery is where the real behaviour lives, and the README documents each option.
fetchPolicy determines whether it should use data cached in the Relay store and whether to send a network request. There are four values. store-or-network is the default: reuse data cached in the store, and if the whole query is cached, skip the network request. store-and-network reuses cached data and always sends a network request. network-only does not reuse cached data and always sends a network request, and the README notes this is the default behaviour of Relay's existing QueryRenderer. store-only reuses cached data and never sends a network request.
Those four are the store-or-network and store-only policies the library set out to implement, plus the other two for completeness.
The remaining options are additions rather than reimplementations, and the README groups them under what is more in relay-hooks.
fetchKey is optional, and it is the refetch lever. A fetchKey can be passed to force a refetch of the current query and variables when the component re-renders, even if the variables did not change, or even if the component is not remounted. The comparison the README draws is to passing a different key to a React component, which causes it to remount. If the fetchKey is different from the one used in the previous render, the current query and variables will be refetched.
That is how you build a refresh button in a Relay application without a separate refetch hook.
networkCacheConfig is optional and holds cache config options for the network layer. The README notes that the network layer may contain an additional query response cache which will reuse network responses for identical queries, and that to bypass this cache completely you pass {force: true} as the value.
skip is optional and, if true, the query will be skipped entirely. That is the hook-level equivalent of a conditional query, and it matters for lists where a variable may not be known yet.
onComplete is optional and is a function that will be called whenever the fetch request has completed. It is the observation hook for the network, and with it you can instrument or trigger side effects after a query lands.
useQuery without suspense, and useLazyLoadQuery with it
The two query hooks differ in one important respect, and the README explains both.
useQuery is the same as useLazyLoadQuery but does not use suspense, and it allows you to use hooks without having to migrate the application in concurrent mode. Its return is the same as the QueryRenderer HOC.
The reason that matters is React's concurrent mode, which for a Relay application means that a query that suspends requires the root to be wrapped in a Suspense boundary and the whole application to be running in concurrent mode. That is a large change to make to adopt a hook library, and useQuery sidesteps it by returning loading state as data rather than suspending.
The useQuery example shows the return shape:
const AppTodo = function (appProps) {
const {data, error, retry, isLoading} = useQuery(query, variables, options);
if (data && data.user) {
return <TodoApp user={data.user} />;
} else if (error) {
return <div>{error.message}</div>;
}
return <div>loading</div>;
}So useQuery returns data, error, retry and isLoading, and the component decides what to render. The isLoading flag is what replaces the suspense fallback, and retry is how you offer a second attempt after a failure.
useLazyLoadQuery is the suspense-shaped one, and the example for it shows the whole stack: a RelayEnvironmentProvider, an ErrorBoundary implemented as a React class component with componentDidCatch, and a React.Suspense boundary inside it.
const App = (
<RelayEnvironmentProvider environment={modernEnvironment}>
<ErrorBoundary fallback={({ error }) => `Error: ${error.message + ': ' + error.stack}`}>
<React.Suspense fallback={<div>loading suspense</div>}>
<AppTodo />
</React.Suspense>
</ErrorBoundary>
</RelayEnvironmentProvider>
);And the component itself destructures only the data:
const AppTodo = function (appProps) {
const {data} = useLazyLoadQuery(query, variables, options);
return <TodoApp user={data.user} />;
}That single-property return is the difference the README lists as a current difference with the upcoming Relay Hooks in react-relay, and it is why the ErrorBoundary is needed in the lazy variant, since there is no isLoading to branch on and no error to inspect from the hook.
The rest of the hook surface, and how the project is built
The remaining hooks are documented in separate files under docs/, each with its own page: useFragment.md, useRefetchable.md, usePagination.md, useMutation.md and useSubscription.md. The names are the GraphQL operations, and each corresponds to a Relay capability: fragments for reading part of a parent record, refetchable for controlled refetching, pagination for cursor-based list queries, mutations for writes and subscriptions for live updates.
The README's own table of contents for the project website lists usefragment, usemutation and usequery among the topics, and the home page is at relay-tools.github.io/relay-hooks/docs/relay-hooks.html.
The build setup tells you how the package is produced and how the GraphQL artifacts are compiled. The relay-compile-test script runs relay-compiler, and the relay key in package.json points the compiler at the ./__tests__ directory, the schema at ./__tests__/testschema.graphql, the language as typescript, and the artifact directory at ./__tests__/__generated__/.
The compile script chains a clean with rimraf, then tsc for the CommonJS build, then tsc against tsconfig.esm.json for the ES module build, then a Babel pass over lib and lib/es, then a replace step driven by replace.config.js, then rollup. The package exposes main at lib/index.js, module at ./lib/es/index.js and unpkg at lib/umd/relay-hooks.min.js, so the three module formats are built by three different stages of that chain.
The runtime dependencies are minimal: @restart/hooks and fbjs. The peer dependency is React, and the range starts at ^16.9.0 or ^17, with the rest truncated in the visible manifest. The examples directory holds examples/relay-hook-example/ and examples/suspense/, which map onto the two modes the hooks support, and the website/ directory holds the documentation site.
The test script runs the relay compiler first, clears the jest cache, and then runs the tests with coverage under NODE_ENV=test, so the GraphQL artifacts are always regenerated before the suite runs.
Where relay-hooks is the wrong choice
Three limits, and the first is about the React version.
The first is the peer dependency range. The manifest declares React at ^16.9.0 or ^17, with the remainder cut off in the visible text. React 18 introduced concurrent rendering as the default, and the whole reason this library exists is to offer a hook API before react-relay had one. Adopting it on React 18 or later means either a peer dependency override or a fork, and at that point the argument for the library is mostly that you like the API shape rather than that you need it, because react-relay now ships hooks of its own with the same names and the same fetch policies.
The second is the naming mismatch. useFragment is useSuspenseFragment here. If you are starting fresh, you are learning a name that is wrong relative to the ecosystem. If you are migrating later, every call site changes. The README is upfront about it in the list of current differences, which is honest, but honesty about an incompatibility is not the same as the incompatibility being cheap.
The third is that a hook library over a compiler-driven framework inherits the framework's machinery without simplifying it. You still need the relay-compiler, you still need a schema, you still need generated artifacts, and you still need to keep the artifacts in step with the query files. The README's own test script runs relay-compile-test before jest for that reason. What relay-hooks removes is the higher-order component ceremony, not the compile step.
The alternative approach for a new project is react-relay's own hooks, useLazyLoadQuery, useFragment, useMutation and useSubscription, which are supported in the current React versions and have the names the rest of the ecosystem uses. The difference in approach is that react-relay's hooks are the reference implementation of the pattern this library anticipated, so anything you learn here transfers, whereas staying here means your knowledge is tied to a library whose reason for existing has been absorbed into the upstream project.
That said, there is a case for this library that has not gone away, and it is the one the README makes. The useQuery hook that does not suspend lets you adopt hooks without migrating the application to concurrent mode. On a large React 17 codebase that cannot move to concurrent rendering for months, that is a genuine constraint rather than a preference, and in that situation relay-hooks is doing something react-relay's hooks do not. The project describes itself as a stable library and none of its dependencies are experimental, which is also relevant: an offline-first Relay application that cannot afford an experimental dependency has a reason to look here.
Editorial conclusion
Adopt relay-hooks if you have a Relay application on React 16 or 17 that wants the hook style without moving to react-relay's own hooks, since the library implements the store-or-network and store-only policies and offers useQuery without suspense so you do not have to migrate an application to concurrent mode. Do not adopt it for a new project on a current React, because react-relay ships hooks of its own and the peer dependency range here stops at React 17. Verify first that your React version is inside the peer dependency range, which the package.json starts at ^16.9.0 or ^17, that the graphql tag and the relay-compiler setup match the query files you write, that the RelayEnvironmentProvider is rendered once at the root of the application, and that the useFragment you need is the renamed useSuspenseFragment rather than the name react-relay later adopted.
Frequently asked questions
What is relay-hooks and what does it provide?
It is a TypeScript library that lets you use Relay from React hooks, exposing the react-relay higher-order components as hooks and implementing the store-or-network and store-only fetch policies. It provides useQuery, useLazyLoadQuery, useFragment under the name useSuspenseFragment, useRefetchable, usePagination, useMutation and useSubscription, plus a RelayEnvironmentProvider.
How do I install relay-hooks?
Install react-relay and relay-hooks together with yarn or npm, using yarn add react-relay relay-hooks. The package itself is published on npm as relay-hooks and the current version in the manifest is 10.0.2.
What fetch policies does relay-hooks support?
Four: store-or-network, which is the default and skips the network request if the whole query is cached, store-and-network, which always sends a request, network-only, which is the default behaviour of Relay's QueryRenderer, and store-only, which never sends a request.
How do I force a refetch with relay-hooks?
Pass a fetchKey in the options object. A fetchKey forces a refetch of the current query and variables when the component re-renders, even if the variables did not change or the component was not remounted, in the same way a different React key causes a remount.
What is the difference between useQuery and useLazyLoadQuery in relay-hooks?
useQuery does not use suspense, so it lets you use hooks without migrating the application to concurrent mode, and it returns the same value as the QueryRenderer HOC with data, error, retry and isLoading. useLazyLoadQuery uses suspense and returns a single data object with the query's data and nothing else.
What React versions does relay-hooks support?
The peer dependency range starts at ^16.9.0 or ^17 in the package manifest. The library also depends on @restart/hooks and fbjs at runtime, and the README describes it as a light library compatible with react-relay.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/relay-tools-relay-hooks)Was this useful?