A pattern of refetching list after list item is added or deleted with apollo graphql

Jimmy Shen
4 min readOct 6, 2017

--

A typical use case dealing with list (e.g. product) is that display products in list (or table) and add a new product in list page, view or edit product detail by clicking the item in the list, and delete product in product detail.

With graphql, add, edit, delete is mutation. When using apollo graphql, the edit mutation is not concern as long as edit mutation return new data with id since data is normalized and referenced by id, so UI everywhere referencing the product with id will get updated. The issue is with add and delete mutation which changes items in the list and the list need to refetch. There are several strategy introduced by apollo docs. When deleting the product by clicking delete button in product detail page, it does not know there are some list page need to refetch list data, therefore manual refetch after delete mutation does not work.

Here introduce a HoC shouldRefetch` to using cache policy to refetch list data. The basic idea is storing timestamp of fetching list and updating list (add or delete list item) in redux store, then comparing these two timestamp to determine whether need to refetch.

shouldRefetchHoC take two parameters name and reduxKey. By default, the timestamp of fetching list and updating list item is stored under time in the redux store. And it can track as many as possible timestamps of different lists which is distinguished by name . The name of timestamp is name suffixed with FetchTime and UpdateTime . e.g. productListFetchTime You don’t necessary need to know it as they are only used internally.

NOTE: elvis is helper function just like groovy elvis operator to safely access property of object.

// ShouldRefetchHoC.jsimport React from 'react'
import PropTypes from 'prop-types'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'

import { elvis } from '../utils'
import { actions } from './ShouldRefetchModule'

const shouldRefetch = (name, reduxKey = 'time') => Component => {
class ShouldRefetch extends React.Component {
render() {
const { fetchTime, updateTime, ...rest } = this.props

if (name) return <Component {...rest} shouldRefetch={updateTime > fetchTime} />

return <Component {...rest} />
}
}

const mapStateToProps = state => {
if (!name) return {}
return {
fetchTime: elvis(state, reduxKey + '.' + name + 'FetchTime') || 0,
updateTime: elvis(state, reduxKey + '.' + name + 'UpdateTime') || 0,
}
}
const mapDispatchToProps = dispatch => ({
shouldRefetchActions: bindActionCreators({ ...actions }, dispatch),
dispatch,
})

return connect(mapStateToProps, mapDispatchToProps)(ShouldRefetch)
}

export default shouldRefetch

SouldRefetchModule export two actions setFetchTime and setUpdateTime which should pass same name with the one of shouldRefetch HoC, and reducer which should be used by root combineReducers to bind it under time in the root of redux store.

// SHouldRefetchModule.jsimport { getNowTimestamp } from '../utils'

const SRFHOC_SET_FETCH_TIME = 'SRFHOC_SET_FETCH_TIME'
const SRFHOC_SET_UPDATE_TIME = 'SRFHOC_SET_UPDATE_TIME'

const setFetchTime = name => dispatch => {
const nowTimestamp = getNowTimestamp()
dispatch({ type: SRFHOC_SET_FETCH_TIME, payload: { [name + 'FetchTime']: nowTimestamp } })
return Promise.resolve({ fetchTime: nowTimestamp })
}

const setUpdateTime = name => dispatch => {
const nowTimestamp = getNowTimestamp()
dispatch({ type: SRFHOC_SET_UPDATE_TIME, payload: { [name + 'UpdateTime']: nowTimestamp } })
return Promise.resolve({ updateTime: nowTimestamp })
}

export const actions = {
setFetchTime,
setUpdateTime,
}

const initialState = {}

export default function shouldRefetchReducer(state = initialState, action) {
let payload = action.payload

switch (action.type) {
case SRFHOC_SET_FETCH_TIME:
case SRFHOC_SET_UPDATE_TIME:
return { ...state, ...payload }
}

return state
}

After reducers are set, It is easy to connect actions to add or delete button and call setUpdateTime after mutation. But to track fetch time, there is a trick to do it when using apollo graphql HoC.

compose(
shouldRefetch('productList'),
graphql(PRODUCT_LIST_QUERY, {
options: ({ shouldRefetch }) => {
return {
fetchPolicy: !shouldRefetch ? 'cache-first' : 'network-only',
variables: { pageIndex: 0 },
}
},
props: ({ data: { loading, me, error, fetchMore } }) => ({
loading
}),
})
)(ProductList)

Use shouldRefetch HoC before graphql query (THIS IS IMPORTANT!), and shouldRefetch will pass a prop shouldRefetch to decorated component which is graphql query, and we can use it to change fetchPolicy to enforce the query retrieve data from server bynetwork-only .

But how to set fetch time of this graphql query, here is the trick. The loading changes to true when fetching data and changes to false when fetching is done. so we call setFetchTime when loading state changes from true to false which is done in the componentWillReceiveProps hook in component ProductList .

class ProductList extends Component {

render() {
...
)
}

componentWillReceiveProps(nextProps) {
const { loading, shouldRefetchActions: { setFetchTime } } = nextProps
if (!loading && this.props.loading) {
setFetchTime('productList')
}
}
}

With these pattern of tracking fetch time of list and update time of list (add or delete list item), you can refetch list automatically when and only when get back to the list page (or component) after add or delete list item.

--

--