Search code examples
javascriptreactjsgraphqlreact-apollographcool

How to approach subscriptions in React app


I need some help to figure out what is a general approach on subscriptions and real-time updating. I have a React Native app and using Apollo and Graphcool service as a backend.

There are a couple scenarios where user looking at the app receives a push notification that something has changed. Naturally, the screen data should get updated as well. Subscriptions are an obvious candidate for that job and I got it working essentially.

I have a subscription like this which works just fine on its own (used to position player avatars on google map).

subscription PlayerMap($gameId: ID) {
  Game(filter: { mutation_in: [CREATED, UPDATED], node: { id: $gameId } }) {
    node {
      players {
        id
        character {
          id
          name
        }
        user {
          id
          latitude
          longitude
        }
      }
    }
  }
}

Then there is a different app screen executing a mutation createPlayer along with refetchQueries from Apollo (for simplicity) which runs this query to update stuff.

query GameCharacters($gameId: ID!) {
  Game(id: $gameId) {
    players {
      id
      character {
        id
        name
      }
    }
  }
}

Now when this completes, the subscription query (that is still active on another screen) also gets updated but for some reason whole Game node is missing in data object.

For handling subscriptions, I have a component like this.

class Subscriber extends Component<void, Props, void> {
  componentDidMount() {
    this.subscribe()
  }
  componentWillReceiveProps({ data, shouldResubscribe }) {
    if (this.unsubscribe) {
      if (shouldResubscribe && shouldResubscribe(data, this.props.data) !== true) {
        return
      }
      this.unsubscribe()
    }
    this.subscribe()
  }
  subscribe() {
    const { data, query, variables } = this.props
    this.unsubscribe = data.subscribeToMore({
      document: query,
      variables,
    })
  }
  unsubscribe: ?Function = null
  render() {
    return this.props.children(this.props.data)
  }
}

I can then use it simply like this with render prop pattern.

const OrgMapScreen = ({ gameId, data: initialData }: Props) => (
  <Subscriber
    data={initialData}
    query={OrgMapSubscription}
    variables={{ gameId }}
    shouldResubscribe={(nextData, prevData) => nextData.Game !== prevData.Game}
  >
    {({ Game }) => {
      const markers = Game.players.map(makePlayerMarker)
      return <MapScreen mapProps={{ markers }} />
    }}
  </Subscriber>
)

I am rather confused why is that happening. Is there some recommended way how to handle stuff like that? Perhaps instead of refetchQueries I should set up another subscription for GameCharacters too?


Solution

  • If I had to guess (not an Apollo expert), I would guess it has to do with either the wrong document input in your subscribeToMore (it seems like you're using the query and not the subscription as the parameter?) or a missing updateQuery in subscribeToMore, which returns the updated data.

    In our case, we have an orderChanged subscription that listens to changes on an Order. When we receive an update, we want to replace the order in our orders query with the updated order. We do so in the updateQuery function in subscribeToMore (apologies for typos, this is not an exact copy-paste):

    componentDidMount() {
      this.props.data.subscribeToMore({
        document: OrderSubscription,
        variables: {
          range: [0, 25]
        },
        updateQuery: (prev, { subscriptionData, }) => {
          // If no subscription data is passed, just return the previous
          // result from the initial `orders` query
          if (!subscriptionData.data) return prev
    
          // get the data for the updated order from the subscription 
          const updatedOrder = subscriptionData.data.orderChanged
    
          // find the index of the updated order from within the existing 
          // array of orders from the `orders` query
          const existingOrderIndex = prev.orders.findIndex(order => (order.id === updatedOrder.id))
    
          // guard for missing data
          if (existingOrderIndex) {
            // replace the old order with the updated order
            prev[existingOrderIndex] = updatedOrder
            // return orders with new, updated data
            return prev
          }
    
          return prev
        },
      })
    }
    

    If you could provide me with a minimum viable repo, I'd be happy to work through it with you. I spent much of last week working through subscriptions :)