Client
In the Getting started section we saw how to query our data from a server-component. In this section we'll see how to mutate data with a server-action and query data from a client-component.
Server actions
We can also invoke mutations as part of a server-action
(opens in a new tab)
This requires us to create a new file, for example in this example we'll call a mutation with a name
argument that returns us Hello ${args.name}
.
We create actions/hello.ts
and give it the content of
'use server'
import { graphql } from '@/fuse'
import { execute } from '@/fuse/server'
import { redirect } from 'next/navigation'
const SayHello = graphql(`
mutation Hello($name: String!) {
sayHello(name: $name)
}
`)
export async function sayHello(args: { name: string }) {
const client = getClient()
const result = await execute({ query: SayHello, variables: { name: args.name } });
console.log(result.data?.sayHello)
// After completing our mutation we perform a redirect
redirect('/')
}
We can use this on the client by doing
import { sayHello } from './actions/sayHello'
const Component = ({ name }) => {
const sayHelloFuse = sayHello.bind(undefined, { name: name || 'fuse' })
return (
<form action={sayHelloFuse}>
<button type='submit'>Say hello and redirect to /</button>
</form>
)
}
Client /app
directory
When you are leveraging the use client;
directive in a /app
component we have opted out
of using server-components. This means we are going back to the traditional way of
distributing our client over React.context
.
It is advisable to create a Provider
component with "use client";
that you use in your
root-layout component so we are enabled to query data in any client page.
import {
Provider,
createClient,
} from '@/fuse/client'
import React from 'react'
export const DatalayerProvider = (props: any) => {
const [client, ssr] = React.useMemo(() => {
const { client, ssr } = createClient({
url: 'http://localhost:3000/api/fuse',
// This is used during SSR to know when the data finishes loading
suspense: true,
})
return [client, ssr]
}, [])
return (
<Provider client={client} ssr={ssr}>
{props.children}
</Provider>
)
}
Let's add this to app/layout.tsx
so we are enabled to query data in any subsequent page.
Querying data cna be done by using the useQuery
hook from your generated fuse folder.
import { useQuery } from '@/fuse/client'
function User() {
const [result] = useQuery({
query: UserQuery,
variables: { id: '1' },
})
}
When you need to reach into your mutatation entry points we supply useMutation
as well.
const UpdateUser = () => {
const [result, update] = useMutation(UpdateUser);
return (
<button onClick={() => update({ id: '1', firstName: 'John' })}>
Update user
</button>
);
}
When you mutate data that is queried from a server-component you will need to call
router.refresh()
to re-render your server-component. The router is a hook exported fromnext/navigation
nameduseRouter
.
For data queried from client-components the client cache will recognise that data got
altered and performa refetch. The cache matches this by means of the __typename
property
that is available on the data.
Heads up, when you query a list of items that is empty we won't be able to infer the
__typename
and you will need to supply it yourself.
const [result] = useQuery({
query: UserQuery,
variables: { id: '1' },
context: useMemo(() => ({ additionalTypenames: ['User'] }), []),
})
React-Client-Components in the /pages
directory
Similar to the /app
directory we can leverage useQuery
the difference being that for server-side
data we will query manually from getServerSideProps
or getStaticProps
and pass it into the component.
import {
useQuery,
withGraphQLClient,
initGraphQLClient,
ssrExchange,
cacheExchange,
fetchExchange,
} from '@/fuse/pages'
function User() {
const [result] = useQuery({
query: UserQuery,
variables: { id: '1' },
})
}
export async function getServerSideProps() {
const ssrCache = ssrExchange({ isClient: false })
const client = initGraphQLClient({
url: 'http://localhost:3000/api/fuse',
exchanges: [cacheExchange, ssrCache, fetchExchange],
})
await client.query(UserQuery, { id: '1' }).toPromise()
const graphqlState = ssrCache.extractData()
return {
props: {
graphqlState,
},
}
}
export default withGraphQLClient((ssrCache) => ({
url: 'http://localhost:3000/api/fuse',
}))(Page)
Performing mutations is done in the same way as in the /app
directory,
with the same caveats.
Best practices
There are some best practices we strongly believe in while developing with fuse
.
Co-locate your fragments
When creating components it's useful to co-locate your data-requirements with your component,
this way when you need more data for your component you know exactly where to add it and
you don't have to go up your component-tree to find the query
responsible for adding the data.
import { FragmentType, graphql, useFragment } from '@/fuse'
import styles from './Avatar.module.css'
const UserFields = graphql(`
fragment Avatar_UserFields on Launch {
firstName
avatarUrl
}
`)
export const Avatar = (props: {
user: FragmentType<typeof UserFields>
}) => {
const user = useFragment(LaunchFields, props.user)
return (
<div styles={styles.avatar}>
<img styles={styles.image} href={user.avatarUrl} alt="...">
<span>Welcome, {user.firstName}</span>
</div>
)
}
The above defined fragment is now globally available and we can add it to our query:
const UserQuery = graphql(`
query User ($id: ID!) {
user(id: $id) {
id
...Avatar_UserFields
}
}
`)
From now on out, every time we need a new field in the Avatar
component we can just add it there
and trust that the query is updated automatically and our data is passed into the component by menans of
<Avatar user={result.data.user} />
.
Top-level queries
One of the benefits that comes with describing the data you need is that you perform 1 request and get 1 response, no waterfall where you need to wait for the list and then perform a whole set of other requests to enrich that data, ... We wrangle the data once, with this comes our sugestion to aggregate the data you need by means of your fragments at the page-level, in doing so you resolve all data in a single request.
With modals/... that pop up you can do one-off requests later when you need the data but not having a waterfall of loading spinners because you are navigating around feels both more performant and more user-friendly.
Adding in-line hints and validation
You can use @0no-co/graphqlsp
to get inline hints while authoring GraphQL documents, you can do so by installing it
with npm i --save-dev @0no-co/graphqlsp
and using the following in your tsconfig.json
:
{
"name": "@0no-co/graphqlsp",
"schema": "./schema.graphql",
"disableTypegen": true,
"templateIsCallExpression": true,
"template": "graphql"
}
When using .vscode
you will need to use the workspace version of TypeScript, to do so you can easily do that by creating
.vscode/settings.json
with the following content
{
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}