gatsby-theme-try-ghost
Version:
A Gatsby theme for building flaring fast blogs from headless Ghost CMS.
146 lines (128 loc) • 5.01 kB
JavaScript
import React from "react"
/**
* Infinite Scroll
*
* Further info 👉🏼 https://github.com/baobabKoodaa/gatsby-starter-infinite-scroll
*
* Global state is needed instead of component state, in order
* to maintain scroll position after page switches.
*
* Global state holds all posts loaded through infinite scroll. It now works
* with different pageContexts and ensures that posts are only loaded once.
*
*/
export const GlobalStateContext = React.createContext({
items: [],
ids: [],
isLoading: true,
useInfiniteScroll: true, /* Fallback in case of error. */
isInitializing: () => true,
hasMore: () => {},
loadMore: () => {},
initItems: () => {},
getItems: () => {},
})
export class GlobalStateProvider extends React.Component {
constructor(props) {
super(props)
this.isInitializing = this.isInitializing.bind(this)
this.hasMore = this.hasMore.bind(this)
this.loadMore = this.loadMore.bind(this)
this.initItems = this.initItems.bind(this)
this.getItems = this.getItems.bind(this)
this.state = {
/* items contains posts which should be rendered
* items is initialized to 1 page of results depending on current context
* 1. render a page to users who have JS disabled
* 2. render initial paint fast for all users
*/
items: [],
ids: [],
/*
* isLoading is used to avoid triggering multiple simultaenous loadings
*/
isLoading: true,
/*
* useInfiniteScroll is used to fallback to pagination _on error_
* note that the fallback to non JS users is not related to this
*/
useInfiniteScroll: true,
isInitializing: this.isInitializing,
hasMore: this.hasMore,
loadMore: this.loadMore,
initItems: this.initItems,
getItems: this.getItems,
}
}
// Internal methods
componentDidMount() {
this.setState({ isLoading: false }) // Allow triggering infinite scroll load
}
exists = id => -1 < this.state.ids.indexOf(id)
findCursor = (pageContext, ids) => pageContext.postIds.reduce((pos, id, i) => (-1 < ids.indexOf(id) && i === pos && i + 1 || pos), 0)
addItems = (pageContext, posts) => {
if (this.state.isInitializing()) {
this.setState(() => {
pageContext.cursor = posts.length
return ({
items: posts,
ids: posts.map(({ node }) => node.id),
})
})
} else {
this.setState((state) => {
const nodes = posts.filter(({ node }) => !state.ids.indexOf(node.id))
const ids = [...state.ids, ...nodes.map(({ node }) => node.id)]
pageContext.cursor = this.findCursor(pageContext, ids)
return ({
items: [...state.items, ...nodes],
ids: ids,
})
})
}
}
// External methods
isInitializing = () => this.state.ids.length === 0
hasMore = pageContext => this.state.useInfiniteScroll && pageContext.cursor < pageContext.postIds.length
loadMore = pageContext => () => {
if (this.state.isLoading || this.isInitializing()) {
return
}
const id = pageContext.postIds[pageContext.cursor]
// id may exist due to previous loads from different pageContext
if (this.exists(id)) {
if (pageContext.cursor < pageContext.postIds.length) {
pageContext.cursor = pageContext.cursor + 1
}
return
}
this.setState({ isLoading: true, error: undefined })
fetch(`${__PATH_PREFIX__}/infiniteScroll/${id}.json`) // eslint-disable-line no-undef
.then(res => res.json())
.then((res) => {
this.setState((state) => {
pageContext.cursor = pageContext.cursor + 1
return ({
items: [...state.items, { node: res }],
ids: [...state.ids, id],
isLoading: false,
})
})
}, (error) => {
this.setState({
isLoading: false,
error,
useInfiniteScroll: false,
})
})
}
initItems = (pageContext, posts) => (this.isInitializing() || !this.useInfiniteScroll) && this.addItems(pageContext, posts)
getItems = pageContext => pageContext.postIds.filter(id => this.exists(id)).map(id => this.state.items[this.state.ids.indexOf(id)])
render() {
return (
<GlobalStateContext.Provider value={this.state}>
{this.props.children}
</GlobalStateContext.Provider>
)
}
}