effector-apollo
A lightweight wrapper around Apollo Client to interoperate with ☄️ Effector.
Create and manage your GraphQL Queries & Mutations declaratively, with Effector, while using powerful Apollo Client features, like normalized cache and Apollo Link.
🔗 Installation & use
$ npm install effector-apollo
$ yarn add effector-apollo
$ pnpm add effector-apollo
Note that this library requires effector@23
and @apollo/client
as peer dependencies, so make sure you have them installed in your project.
API
createQuery
Creates a new query that allows you to read data from GraphQL server or cache on request.
Options:
client
: ApolloClient | Store<ApolloClient>
that the query will use to fetch datadocument
: DocumentNode
describing your querycontext
: DefaultContext?
allows you to (optionally) provide arbitrary context to your Apollo Link
Returns: a new Query
instance
Query
Commands:
start
: EventCallable<Variables>
unconditionally starts your query, and will send a network requestrefresh
: EventCallable<Variables>
will refresh the query, using cache if possiblereset
: EventCallable<void>
resets your query to its initial state
Query state:
$data
: Store<Data | null>
containing your query data$error
: Store<ApolloError | null>
possibly containing query execution error$status
: Store<'initial' | 'pending' | 'done' | 'fail'>
reflecting current status of your query, which is also split into separate 'convenience' stores
$idle
: true
if the Query
has not ever started$pending
: true
if the Query
is currently fetching$failed
: true
if the Query
has failed with some ApolloError
$succeeded
: true
if the Query
has succeeded with data$finished
: true
if the Query
has finished with either success or failure
Query state events:
finished.success
: Event<{ variables: Variables; meta: QueryMeta, data: Data }>
is fired when your Query
succeeds, providing variables
that you called the query with, and data
that the query has returnedfinished.failure
: Event<{ variables: Variables; meta: QueryMeta, error: ApolloError }>
is fired when your Query
fails, providing variables
and the corresponding error
finished.finally
: Event
is fired when your Query
finishes with either status: "done"
or status: "fail"
, and will provide you with data
/error
Example usage
const query = createQuery({
client,
document: gql`
query user {
user {
id
name
}
}
`,
})
sample({
clock: appStarted,
target: query.start,
})
createMutation
Creates a new mutation to modify data on your GraphQL server.
Options:
client
: ApolloClient | Store<ApolloClient>
that the mutation will use to fetch datadocument
: DocumentNode
describing your mutationcontext
: DefaultContext?
allows you to (optionally) provide arbitrary context to your Apollo Link
Returns: a new Mutation
instance
Mutation
Commands:
start
: EventCallable<Variables>
unconditionally starts your Mutation
, and will send a network request immediatelyreset
: EventCallable<void>
resets your mutation to its initial state
Mutation state:
$status
: Store<'initial' | 'pending' | 'done' | 'fail'>
reflecting current status of your Mutation
, which is also split into separate 'convenience' stores
$idle
: true
if the Mutation
has not ever started$pending
: true
if the Mutation
is currently fetching$failed
: true
if the Mutation
has failed with some ApolloError
$succeeded
: true
if the Mutation
has succeeded with data$finished
: true
if the Mutation
has finished with either success or failure
Mutation state events:
finished.success
: Event<{ variables: Variables; data: Data }>
is fired when your Mutation
succeeds with data
that the GraphQL server has returnedfinished.failure
: Event<{ variables: Variables; error: ApolloError }>
is fired when your Mutation
fails with the corresponding execution error
finished.finally
: Event
that's fired when your Mutation
finishes with either status: "done"
or status: "fail"
, and will provide you with data
/error
createFragmentBinding
createFragmentBinding
sets up a light, read-only binding into Apollo Cache. This can come in handy to access entities from your Apollo Cache without requiring a specific Query
to do that.
Like useFragment
from Apollo Client, fragment binding does not trigger network requests on its own. Rather, it keeps data in sync between Apollo Cache and Effector.
Unlike useFragment
, however, createFragmentBinding
's primary purpose is to provide you with singleton-like entities that your business logic might requre. Common examples would be: currently logged in User
entity, a specific Setting
, or feature flags.
Options:
-
client
: ApolloClient | Store<ApolloClient>
that provides cached data
-
document
: DocumentNode
with a single fragment
definition you want to bind
-
id
: Store<string> | Store<StoreObject>
to identify a specific fragment
-
When provided with Store<string>
, binding will treat this as a ready-to-use Canonical Cache ID (like User:137
)
-
When provided with Store<StoreObject>
, binding treats this as an object with key fields that uniquely identify your fragment
Unless you customize your cache ID,
id
or _id
are your key fields. So, Store<{ id: string }>
is usually the way to go!
If you did customize cache ID for your fragment, provide all necessary key fields set in your type policy.
💡 You do not need to provide __typename
in this Store
, as it is inferred from your fragment
.
-
variables?
: Store<Variables>
that your fragment
requires to resolve
- You can omit this if your
fragment
does not require any variables - ⚠️ If the
fragment
does need variables, you must provide this option with correct Variables
, otherwise you'll never receive data
-
setup
: Event
that the binding will initialize on (upon trigger)
- Usually, that would be your
appStarted
event, but you can also use any other trigger, like Query.finished.success
-
teardown
: Event
tears down the binding, clearing $data
and stopping listening for updates
-
optimistic?
: boolean
can be set to false
to disable reading optimistic
cache
Returns: a new FragmentBinding
FragmentBinding
A live binding into ApolloCache
for a specific fragment.
$data
: Store<Data | null>
containing your fragment
data (or null
if no fragment
was found in Cache)$active
: Store<boolean>
marking if your binding is active or not
watchQuery
watchQuery
allows you to subscribe a particular query to Apollo Cache. By default, Query
only reads data through a request, and does not read cache (unlike Apollo's React hooks).
This operator allows you to connect Query
to cache if you expect other parts of your app to request the same query. This will help you avoid extra requests.
Options:
query
: Query
that you want to subscribe to cacheoptimistic?
: boolean
can be set to false
to disable reading optimistic
cacheclient?
: ApolloClient | Store<ApolloClient>
to use cache from. Will use the client from createQuery
if not provided
keepFresh
Enables automatic refreshes for a query, ensuring that the data stays up-to-date in response to specific events or triggers.
Options:
query
: Query
you want to keep freshtriggers
: Array<Event | TriggerProtocol>
containing triggers that will invalidate the query and initiate a network request for fresh data. Trigger can be either:
enabled?
: Store<boolean>
that controls whether the automatic refresh is enabled or disabled. If ommited, defaults to always enabled
Example usage:
Fetch the query when network connection is restored
import { keepFresh, createQuery } from "effector-apollo"
import { trackNetworkStatus } from "@withease/web-api"
const userQuery = createQuery({ client, document: USER_QUERY })
keepFresh(userQuery, {
triggers: [trackNetworkStatus],
})
Refetch a related query when a mutation fails
import { keepFresh, createQuery, createMutation } from "effector-apollo"
const userQuery = createQuery({ client, document: USER_QUERY })
const updateCountryMutation = createMutation({ client, document: UPDATE_COUNTRY_MUTATION })
keepFresh(userQuery, {
triggers: [updateCountryMutation.finished.failure],
})
paginate
paginate
creates a fetchMore
-like function for your query, allowing for easy pagination powered by Apollo Cache.
Note that this operator requires two things from you to work properly:
- You must define a proper Field Policy on a paginated field using a
merge
function of InMemoryCache
. This allows to store paginated results in the cache - The
Query
must not be "idle" when you use pagination. Fetch the Query using .refresh
or .start
before paginating.
Triggering the event returned by paginate(query)
will
- shallowly merge last used
query
variables and new variables provided to paginate
- execute the
query
to fetch the next page based on updated variables - merge pages using your policy (as defined in
InMemoryCache
) - store results in
query.$data
Read more about Pagination API in Apollo.
Options:
query
: Query
that will be paginated
Returns: EventCallable<Partial<Variables>>
which you can call to fetch the next page
Example usage:
Paginate a query using a cursor in a Relay-style pagination.
import { createQuery, paginate } from "effector-apollo"
import { gql } from "@apollo/client"
const document = gql`
query list($cursor: String) {
items(cursor: $cursor) {
nodes {
id
}
pageInfo {
endCursor
}
}
}
`
const listQuery = createQuery({ client, document })
const nextPageRequested = createEvent<void>()
sample({
clock: nextPageRequested,
source: listQuery.$data.map(({ items }) => items.pageInfo.endCursor),
fn: (cursor) => ({ cursor }),
target: paginate(listQuery),
})
optimistic
optimistic
helps you define an optimistic response for your mutation. This will fill in data in Apollo Cache when running the mutation, so that your UI is responsive to user action. See more in Apollo Client "Optimistic results" documentation.
Options:
mutation
: Mutation
that you want to define an optimistic response forfn
: Variables => Data
function that constructs an optimistic response for a mutationclient?
: ApolloClient | Store<ApolloClient>
to write response to. Will use the client from createMutation
if not provided
💬 Pick your library
When starting a new project from scratch, please, take a look at Farfetched, a great data fetching tool, before using effector-apollo
.
This library is an interoperability layer for projects that already use Apollo Client. It makes your life easier by giving you access to your GraphQL data from Effector.
This library strives to keep its API similar to Farfetched so that your future migration to this tool is simpler.
Releases policy
Versions 0.x.x
may contain breaking changes in minor releases, which will be documented.