Skip to content

Latest commit

 

History

History
397 lines (307 loc) · 11.9 KB

archives.md

File metadata and controls

397 lines (307 loc) · 11.9 KB

Archives

An archive is any list of objects associated with an ID. For performance, archives are registered up-front, and their results are cached.

Archives can be registered in one of two ways:

// Register archives with static arguments.
posts.registerArchive( 'home', {} );
posts.registerArchive( 'stickied', { sticky: '1' } );
posts.registerArchive( 'oldest', {
	order: 'desc',
	orderby: 'date',
} );

// Or, register a function which returns the arguments:
posts.registerArchive( 'today', () => {
	return {
		after:  moment().startOf( 'day' ).toISOString(),
		before: moment().endOf( 'day' ).toISOString(),
	}
} );

// The function is also passed the full state, if you want to use it:
posts.registerArchive( 'mine', state => {
	return {
		author: state.user.id,
	}
} );

withArchive

You can use the withArchive HOC to access your data in your components:

// TodayArchive.js
import { withArchive } from '@humanmade/repress';
import React from 'react';

import { posts } from './types';

const TodayArchive = props => <ul>
	{ props.posts.map( post =>
		<li key={ post.id }>
			{ post.title.rendered }
		</li>
	) }
</ul>;

export default withArchive(
	// Handler object:
	posts,

	// getSubstate() - returns the substate
	state => state.posts,

	// Archive ID
	'today'
)( TodayArchive );

withArchive components will automatically load the archive if it's not available in the store. Your component receives five data props, and two action props:

  • archiveId (mixed): The (resolved) ID for this archive.
  • posts (object[]): A list of objects in the archive.
  • loading (boolean): Whether the archive is currently being loaded.
  • hasMore (boolean): Whether there are more pages of the archive to load.
  • loadingMore (boolean): Whether the next page of the archive is being loaded.
  • onLoad (Function: () => Promise): Loader function. Called automatically by the HOC, but you can call this again if needed.
  • onLoadMore (Function: ( page = null ) => Promise): Loader function. Call this to load the next page of the archive, or pass a page number to load that specific page.

For convenience, you might want to make your own HOC to simplify this to just an ID:

import { withArchive } from '@humanmade/repress';

import { posts } from './types';

export default id => withArchive( posts, state => state.posts, id );

Rather than passing a static ID, you can also pass an ID function, which will be called with your props. If you're using React Router, this allows you to easily use your route's path as the archive ID:

export const normalizePath = path => path.replace( /^\/+|\/+$/g, '' );
export default withArchive(
	posts,
	state => state.posts,
	props => normalizePath( props.match.path )
)( TodayArchive );

(The resolved ID will be passed to your component as archiveId.)

Advanced Options

You can pass a fourth parameter called options to withArchive. This is an object with the following keys:

  • mapDataToProps (Function: object => object): Map the data props to props passed to your component. By default, this passes through all data props.
  • mapActionsToProps (Function: object => object): Map the action props to props to your component. By default, this passes through all action props.

useArchive

You can alternatively use the useArchive hook to access your data in your components:

// TodayArchive.js
import { useArchive } from '@humanmade/repress';
import React from 'react';

import { posts } from './types';

export default function TodayArchive( props ) {
	const archive = withArchive(
		// Handler object:
		posts,

		// getSubstate() - returns the substate
		state => state.posts,

		// Archive ID
		'today'
	);
	return (
		<ul>
			{ archive.posts.map( post =>
				<li key={ post.id }>
					{ post.title.rendered }
				</li>
			) }
		</ul>
	);
}

The useArchive hook will automatically load the archive if it's not available in the store. The hook returns four data props, and two action props:

  • posts (object[]): A list of objects in the archive.
  • loading (boolean): Whether the archive is currently being loaded.
  • hasMore (boolean): Whether there are more pages of the archive to load.
  • loadingMore (boolean): Whether the next page of the archive is being loaded.
  • load (Function: () => Promise): Loader function. Called automatically by the HOC, but you can call this again if needed.
  • loadMore (Function: ( page = null ) => Promise): Loader function. Call this to load the next page of the archive, or pass a page number to load that specific page.

Pagination

Pagination support for archives is included out of the box. You can use either infinite scroll style ("more-style") or manual pagination.

With more-style pagination, your component will receive all posts Repress has loaded. When you load more posts, it will append these to the list of posts, and your component will receive this via the posts prop.

With manual pagination, you specify which page of the archive to load, and your component will only receive posts on that page of the archive. When you load a different page, Repress will only pass the posts for that page. Repress will keep other pages in memory allowing for fast pagination back to already-loaded pages.

Use more-style pagination when you want an infinite scroll list of posts that expands with additional items, or if you plan on loading the entire archive into memory. Use manual pagination when you want to show a subset of posts rather than the whole archive, or if the page number is externally controlled (e.g. in the URL).

More-Style Pagination

To load the next page in an archive, simply call the onLoadMore prop passed in by withArchive. Repress keeps track of which page you're accessing, and appends additional posts to the existing list of posts.

You usually only want to call onLoadMore if there actually is more to load, so you should check hasMore before calling the function. Also, you should typically only call onLoadMore based on user input (a button, link, scroll handler, etc); if you need more posts on load, increase the per_page parameter in your query instead.

For example, the following component renders a simple list of posts with a button to allow fetching more:

function Blog( props ) {
	const { hasMore, loading, loadingMore, posts } = props;

	if ( loading ) {
		return <div>Loading…</div>;
	}

	if ( ! posts ) {
		return <div>No posts</div>;
	}

	return <div>
		<ol>
			{ posts.map( post =>
				<li key={ post.id }>{ post.title.rendered }</li>
			) }
		</ol>

		{ loadingMore ?
			<p>Loading more…</p>
		: hasMore ?
			<button
				type="button"
				onClick={ () => props.onLoadMore() }
			>Load More</button>
		: null }
	</div>;
}
export default withArchive( posts, state => state.posts, 'blog' )( Blog );

Or, with hooks:

export default function Blog( props ) {
	const archive = useArchive( posts, state => state.posts, 'blog' );

	if ( archive.loading ) {
		return <div>Loading…</div>;
	}

	if ( ! archive.posts ) {
		return <div>No posts</div>;
	}

	return (
		<div>
			<ol>
				{ posts.map( post => (
					<li key={ post.id }>{ post.title.rendered }</li>
				) ) }
			</ol>

			{ archive.loadingMore ? (
				<p>Loading more…</p>
			) : archive.hasMore ? (
				<button
					type="button"
					onClick={ () => archive.loadMore() }
				>
					Load More
				</button>
			) : null }
		</div>
	);
}

Manual Pagination

For manual pagination, use the withPagedArchive HOC instead of withArchive. This allows you to manually specify the page of the archive to load, and ignores Repress's internal page counter. Additionally, only the current page of items is passed to your component.

This HOC passes the same props as withArchive, with the following differences:

  • posts (object[]): A list of objects on the given page of the archive.
  • page (Number): The current page being viewed.
  • totalPages (Number): Total number of pages available for the archive.

The archive page to load should be passed via the page prop to your component. To override this behaviour, you can pass a getPage function in the options parameter to the HOC:

  • getPage (Function: object => Number): Map passed props to the page number. By default, this is props => props.page.

onLoadMore is automatically called for you when the page changes. You can use this.props.hasMore to determine whether to show navigation to the next page, and you can check this.props.page > 1 to determine whether to show navigation to the previous page.

For example, the following component renders a simple list of posts with navigation to browse back and forth:

function Blog( props ) {
	const { hasMore, loading, loadingMore, page, posts } = props;

	if ( loading ) {
		return <div>Loading…</div>;
	}

	if ( ! posts ) {
		return <div>No posts</div>;
	}

	return <div>
		<ol>
			{ posts.map( post =>
				<li key={ post.id }>{ post.title.rendered }</li>
			) }
		</ol>

		{ loadingMore ?
			<p>Loading more…</p>
		: (
			<div>
				{ hasMore ? (
					<button
						type="button"
						onClick={ () => props.onOlder() }
					>Older Posts</button>
				) : null }
				{ page > 1 ? (
					<button
						type="button"
						onClick={ () => props.onNewer() }
					>Newer Posts</button>
				) : null }
			</div>
		: null }
	</div>;
}
const PagedBlog = withPagedArchive( posts, state => state.posts, 'blog' )( Blog );

// A higher-level component controls the page.
class App extends React.Component {
	state = {
		page: 1,
	}

	render() {
		const { page } = this.state;
		return (
			<PagedBlog
				page={ page }
				onOlder={ () => this.setState( { page: page + 1 } ) }
				onNewer={ () => this.setState( { page: page - 1 } ) }
			/>
		);
	}
}

Manual Pagination with Hooks

This manual pagination style can be condensed into a single component by using hooks instead.

Just like the useArchive hook, there's also a usePagedArchive hook. This hook returns the same props as useArchive, with the following differences:

  • posts (object[]): A list of objects on the given page of the archive.
  • totalPages (Number): Total number of pages available for the archive.

The usePagedArchive hook automatically calls loadMore for you when the page changes. You can use hasMore to determine whether to show navigation to the next page, and you can check page > 1 to determine whether to show navigation to the previous page.

function Blog( props ) {
	const [ page, setPage ] = useState( 1 );
	const archive = withPagedArchive( posts, state => state.posts, 'blog', page );

	if ( archive.loading ) {
		return <div>Loading…</div>;
	}

	if ( ! archive.posts ) {
		return <div>No posts</div>;
	}

	return (
		<div>
			<ol>
				{ posts.map( post => (
					<li key={ post.id }>{ post.title.rendered }</li>
				) ) }
			</ol>

			{ archive.loadingMore ? (
				<p>Loading more…</p>
			) : (
				<div>
					{ archive.hasMore ? (
						<button
							type="button"
							onClick={ () => setPage( page + 1 ) }
						>
							Older Posts
						</button>
					) : null }
					{ page > 1 ? (
						<button
							type="button"
							onClick={ () => setPage( page - 1 ) }
						>
							Newer Posts
						</button>
					) : null }
				</div>
			: null }
		</div>
	);
}

Dynamic Archives

If needed, you can dynamically register archives when needed. Note that you'll need the archive ID to access the results, so make sure it's predictable. You should only do this when you can't determine the ID beforehand (such as in dynamic routes), as it tends to make your code less readable.

export const search = term => {
	posts.registerArchive( `search/${ term }`, {
		search: term,
		orderby: 'relevance',
	} );
	return posts.fetchArchive( `search/${ term }` );
};

export const getYearArchive = year => {
	posts.registerArchive( `years/${ year }`, {
		after:  moment( { year } ).startOf( 'year' ),
		before: moment( { year } ).endOf( 'year' ),
	} );
	return posts.fetchArchive( `years/${ year }` );
};