Documentation
Basics
Client

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 from next/navigation named useRouter.

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
}