Dac0d3r
Dac0d3r

Reputation: 1844

How to best access data from QueryRenderer in a parent component in Relay Modern?

As you can see from the picture below, I'm rendering the popover using a react-relay QueryRenderer since the request is slow, and I do not want the rest of the page to wait for events to be fetched.

My problem is that in the navigation I have a button to show/hide the popover. That button should only be rendered when events has loaded, and the button also needs to show a count of how many events there is.

So my question is how to pass events data up from QueryRenderer (popover) to a parent component (toggle button)?

My first idea was to reuse my QueryRenderer for events and pass in dataFrom={'STORE_ONLY'}, to avoid a new HTTP request and use the cache instead, but unfortunately 'STORE_ONLY' is not an option... YET...

From looking at https://github.com/relay-tools/relay-hooks/issues/5 it seems like store-only will be supported by useQuery in the future, so is that the recommended solution to go about it, or how is the recommended way? Surely facebook, and many other applications, must have had this need frequently?

enter image description here

Upvotes: 1

Views: 892

Answers (1)

BorisTB
BorisTB

Reputation: 1806

You can achieve redux-like relay store with custom handlers and local schema.

I'll be guessing what your queries, components and fields might be named like so don't forget to change it to correct values

Somewhere in project's src folder create a file ClientState.client.graphql to extend your root query type with new field for client state:

// ClientState.client.graphql

type ClientState {
  showToggleButton: Boolean!
  eventsCount: Int
}

extend type Query {
  clientState: ClientState!
}

this will allow you to wrap Toggle button with fragment like this:

fragment ToggleButton_query on Query {
  clientState {
    showToggleButton
    eventsCount
  }
}

and spread this fragment in parent query (probably AppQuery)

Then in your second query, where you'll be fetching events, add @__clientField directive, to define custom handle for that field:

query EventModal {
  events @__clientField(handle: "eventsData") {
    totalCount
  }
}

Create EventsDataHandler for handle eventsData:

// EventsDataHandler.js

// update method will be called every time when field with `@__clientField(handle: "eventsData")` is fetched

const EventsDataHandler = {
  update (store, payload) {
    const record = store.get(payload.dataID)

    if (!record) {
      return
    }

    // get "events" from record
    const events = record.getLinkedRecord(payload.fieldKey)

    // get events count and set client state values
    const eventsCount = events.getValue('totalCount')
    const clientState = store.getRoot().getLinkedRecord('clientState')

    clientState.setValue(eventsCount, 'eventsCount')
    clientState.setValue(true, 'showToggleButton')

    // link "events" to record, so the "events" field in EventModal is not undefined
    record.setLinkedRecord(events, payload.handleKey)
  }
}

export default EventsDataHandler

Last thing to do is to assign custom (and default) handlers to environment and create init store values:

// environment.js

import { commitLocalUpdate, ConnectionHandler, Environment, RecordSource, Store, ViewerHandler } from 'relay-runtime'
import EventsDataHandler from './EventsDataHandler'

// ...

const handlerProvider = handle => {
  switch (handle) {
    case 'connection':
      return ConnectionHandler
    case 'viewer':
      return ViewerHandler
    case 'eventsData':
      return EventsDataHandler
    default:
      throw new Error(`Handler for ${handle} not found.`)
  }
}

const environment = new Environment({
  network,
  store,
  handlerProvider
})

// set init client state values
commitLocalUpdate(environment, store => {
  const FIELD_KEY = 'clientState'
  const TYPENAME = 'ClientState'

  const dataID = `client:${FIELD_KEY}`

  const record = store.create(dataID, TYPENAME)

  record.setValue(false, 'showToggleButton')

  // prevent relay from removing client state
  environment.retain({
    dataID,
    variables: {},
    node: { selections: [] }
  })

  store.getRoot().setLinkedRecord(record, FIELD_KEY)
})

Upvotes: 4

Related Questions