1

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

Dac0d3r
  • 2,176
  • 6
  • 40
  • 76
  • You can actually render that button from the query renderer’s child with Portals https://reactjs.org/docs/portals.html – BorisTB Aug 24 '19 at 01:18
  • Thanks @Boris a portal will work, but is not optimal, but thanks... What I'm after is how to do something similar to having fragments in MyEventsToggleButton and EventModal components under the AppQueryRenderer. The problem is just that I can't fetch events inside the AppQueryRenderer (root renderer), because it takes a long time to load, and therefore will block the other data in that AppQuery. But surely you should be able to get the same slice of state in components in different places of the react tree? Like context or even redux where you can connect to the state of interest? – Dac0d3r Aug 24 '19 at 18:18
  • 1
    `@defer` directive would be ideal for this, sadly it's not supported :/ https://github.com/facebook/relay/issues/2194 I will try to describe how to achieve redux-like behavior in answer – BorisTB Aug 25 '19 at 19:30
  • Thanks. Very disappointed if relay can't handle this in an elegant way. Should be very basic. – Dac0d3r Aug 25 '19 at 21:29

1 Answers1

4

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)
})
BorisTB
  • 1,686
  • 1
  • 17
  • 26
  • Wow. Thank you sir for this thorough answer. I created a bounty just before, so I'll let it run a few days, but unless somebody comes up with a more simple relay-way of doing this, I'll of course accept your answer and reward it with a bounty (and advice my company to not pick relay, since this is waaay to much code for doing a trivial thing). Again, thank you very much! – Dac0d3r Aug 26 '19 at 18:47
  • I ran into a problem and got this compile error `RelayFieldHandleTransform: Expected fields to have at most one "handle" property, got `[object Object], [object Object]`.` I guess its because events already have a directive and now looks like this: `@__clientField(handle: "eventsData") @connection(key: "EventList_events"....` If I remove @__clientField it compiles again. – Dac0d3r Aug 26 '19 at 20:05
  • Oh right, when it's a connection it's little harder, so probably use that directive on totalCount field and change handler little bit (`const eventsCount = record.getLinkedRecord(payload.fieldKey)`). Or you can set custom handler on connection with `@connection(handler: "customHandle" key: "connection_key")` but it's quite complicated to handle connection properly. https://github.com/facebook/relay/blob/master/packages/relay-runtime/handlers/connection/RelayConnectionHandler.js – BorisTB Aug 27 '19 at 11:46