A pattern of refetching list after list item is added or deleted with apollo graphql
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.