react-floodgate
v1.0.0
Published
Configurable and flexible "load more" component for React
Downloads
13
Maintainers
Readme
The motivation
I have worked on a few client sites and side projects where serialized data is to be displayed concatenated to a given length, with the ability to load more entries after a respective user interaction.
This can easily result in a complicated mixture of Array.splice
-ing, potential data mutation, and overly complicated component methods.
Surely there can be a more elegant solution?
This solution
Enter react-floodgate
; like its namesake, this component allows for the precise and safe control of resources. Using an ES2015 generator function as the control mechanism and the function-as-child pattern for flexible and developer-controlled rendering, one can load serialized data into react-floodgate
, render their desired components, and safely and programmatically iterate through the data as needed.
The inspiration
This project was inspired by Kent Dodd's Downshift, this talk by Ryan Florence, and this blog post by Max Stoiber.
This README file modeled after the Downshift README.
Installation
You can install the package via npm
or yarn
:
$ yarn add react-floodgate
or
$ npm i --save react-floodgate
Usage
This is a basic example of Floodgate, showcasing an uncontrolled implementation:
const BasicExample = props => (
<Floodgate
data={[4, 8, 15, 16, 23, 42]}
initial={3}
increment={1}
exportStateOnUnMount={false}
onLoadNext={(stateAtLoadNext) => console.log(stateAtLoadNext)}
onLoadAll={(stateAtLoadAll) => console.log(stateAtLoadAll)}
onReset={(stateAtReset) => console.log(stateAtReset)}>
{({ items, loadNext, loadAll, reset, loadComplete }) => (
<div>
<ul>
{items.map(number => <li key={number}>{number}</li>)}
</ul>
<button onClick={loadNext} disabled={loadComplete}>Load More</button>
<button onClick={loadAll} disabled={loadComplete}>Load All</button>
{loadComplete ? <button onClick={reset}>Reset</button> : null}
</div>
)}
</Floodgate>
)
Uncontrolled Floodgate components are entirely static, and their state will be complete lost/reset when unmounting and re-mounting. In order to ensure internal state is saved during these scenarios, and in order to create dynamic Floodgate components, Floodgate has to be controlled.
Controlled Floodgate
The following is a basic example of a controlled Floodgate implementation; this component has a location to save Floodgate state, and uses those values as Floodgate's props. In order to make sure this component does save Floodgate's state, the onExportState
prop will have to have a function passed to it that saves desired Floodgate state properties to the controlling component's state.
class FloodgateController extends React.Component {
constructor(props) {
super();
this.state = {
showFloodgate: true,
FGState: {
data: props.data,
initial: 3,
increment: 3
}
};
this.toggle = this.toggle.bind(this);
}
toggle() {
this.setState(prevState => ({
showFloodgate: !prevState.showFloodgate
}));
}
render() {
return (
<div>
<button onClick={this.toggle}>Toggle Floodgate</button>
{this.state.showFloodgate ? <Floodgate
data={this.state.FGState.data}
increment={this.state.FGState.increment}
initial={this.state.FGState.initial}
exportStateOnUnmount={true}
onExportState={newFGState => this.setState(prevState => ({
FGState: {
...prevState.FGState,
...newFGState,
initial: newFGState.currentIndex
}
}))}>
{({ items, loadNext, loadAll, reset, loadComplete }) => (
<div>
<ul>
{items.map(number => <li key={number}>{number}</li>)}
</ul>
<button onClick={loadNext} disabled={loadComplete}>Load More</button>
<button onClick={loadAll} disabled={loadComplete}>Load All</button>
{loadComplete ? <button onClick={reset}>Reset</button> : null}
</div>
)}
</Floodgate> : null }
</div>
);
}
}
const ControlledFGInstance = <FloodgateController data={[4, 8, 15, 16, 23, 42]} />;
This strategy can also be employed to fetch data to pass into Floodgate's data
prop, or alongside some settings dialogue to allow end-users control over how this feed behaves.
API
Floodgate
props
| name | type | default | description |
|------------------------|-------------|--------------|-------------------------------------------------------------------------------------------------------------|
| data
| Array<any> | null
| The array of items to be processed by Floodgate
| |
| initial
| number | 5
| How many items are initially available in the render function| |
| increment
| number | 5
| How many items are added when calling loadNext
| |
| exportStateOnUnmount
| boolean | (optional) | Toggle if exportState
will be called during componentWillUnmount
|
| onExportState
| Function | (optional) | Function to pass up Floodgate's internal state when componentWillUnmount
fires or exportState
is called |
| onLoadNext
| Function | (optional) | Callback function to run after loadNext
; runs after inline callback
argument prop |
| onLoadComplete
| Function | (optional) | Callback function to run after loadComplete
; runs after inline callback
argument prop |
| onReset
| Function | (optional) | Callback function to run after reset
; runs after inline callback
argument prop |
data
Type: Array<any> = null
The array of items to be processed by the Floodgate
internal queue.
This array will accept any type of element, but it is recommended to either provide elements with a uniform type, or normalize elements before they get consumed by Floodgate
. This best practice is to safeguard against the possibility of performing side effects on an element in Floodgate's render
function that are incompatible with a given element's type; e.g. an element with a type of { name: 'Jane Doe', email: '[email protected]' }
, but in the render
function performing exampleItem.toUpperCase()
.
initial
Type: number = 5
The length of the first set of items that will be rendered from Floodgate.
increment
Type: number = 5
The length of subsequent sets of items when calling loadNext
.
exportStateOnUnmount
Type: boolean = false
Flag to configure the calling of props.onExportState
when Floodgate triggers the componentWillUnmount
component lifecycle event.
onExportState
Arguments: { currentIndex: number, renderedItems: any[], allItemsRendered: boolean }
Prop callback function that executes when Floodgate triggers the componentWillUnmount
component lifecycle event, or when the exportState
is called from the render prop function. It provides a single object argument that represents a set of internal state properties that can be exported to a different component; this is best used on instances that will be toggled (un)mounted, such as in tabs or a single page application.
currentIndex
is a number representing the index of the last item passed through the queue to state.renderedItems
.
renderedItems
is an array of all items that have been passed through the queue from props.data
.
allItemsRendered
a boolean describing if all items have been processed by the queue.
onLoadNext
Arguments: Floodgate.state
Callback property that fires after the loadNext
method is called. This is executed after loadNext
's callback
method is executed.
onLoadComplete
Arguments: Floodgate.state
Callback property that fires after the loadComplete
method is called. This is executed after loadComplete
's callback
method is executed.
onReset
Arguments: Floodgate.state
Callback property that fires after the reset
method is called. This is executed after reset
's callback
method is executed.
render
function
Note: the render
function uses a single object argument to expose the following values/functions. Use the ES2015 destructuring syntax to get the most of this pattern. (see the Usage and Examples sections on how to do this)
| name | type | default | parameters | description |
|----------------|-------------|---------|-------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| items
| Array<any> | null
| n/a | State: the subset of items determined by the intitial
and increment
props| |
| loadComplete
| boolean | false
| n/a | State: describes if all items have been processed by the Floodgate
instance| |
| loadAll
| Function | n/a | {callback?: Function}
| Action: loads all items
; callback
prop in argument fires immediately after invocation| |
| loadNext
| Function | n/a | {silent?: boolean, callback?: Function}
| Action: loads the next set of items; callback
prop in argument fires immediately after invocation, silent
determinse if onLoadNext
callback is fired after calling loadNext
| |
| reset
| Function | n/a | {callback?: Function}
| Action: resets the state of the Floodgate
instance to the initial state; callback
prop in argument fires immediately after invocation| |
| exportState
| Function | n/a | null
| Action: calls the onExportState
prop callback| |
items
Type: Array<any> = null
Subset of all elements in the props.data
array, based on the values of the initial
and increment
props.
Elements of items
do not have to be rendered at all; for example, props.data
could be comprised of string manipulation methods, and each member of items
would then call the respective method on a static value.
loadComplete
Type: boolean = false
Describes if all elements of the props.data
array have been processed by the internal queue and passed to items
.
loadAll
Arguments: { suppressWarning?: boolean, callback?: Function } = { suppressWarning: false }
Appends all elements currently in the data
prop to the items
array. When called, the render
argument's loadComplete
property will be set to true
, and the currentIndex
state property will be updated to the length of Floodgate.props.data
.
The supressWarning
argument property determines if a warning should be emitted when all items are rendered`.
The callback
argument method will be called after loadAll
has set the component's state; it will have access to this updated Floodgate state
.
loadNext
Arguments: { silent?: boolean, callback?: Function } = { silent: false }
Appends the next elements in the data
prop to the items
array, length equal to the increment
prop. When called, will update the currentIndex
state property; if this increment is equal to or exceeds the length of data
, the render
argument's loadComplete
property will be set to true
.
The silent
argument property determines if this call triggers the onLoadNext
prop callback.
The callback
argument method will be called after loadNext
has set the component's state; it will have access to this updated Floodgate state
.
reset
Arguments: { initial?: number, callback?: Function } = {}
Resets Floodgate's state to the current instance's data
and initial
prop values.
The initial
argument property provides the ability to pass in a custom initial
value to the next rendering after reset
is called; this is most useful when writing a controlled Floodgate component and the onExportState
prop is used. For more information on why this is needed, see pull request #42.
The callback
argument method will be called after reset
has set the component's state; it will have access to this updated Floodgate state
.
exportState
Arguments: n/a
Calls the onExportState
prop callback. Any logic to manipulate and/or save Floodgate's state to a parent component should happen in that prop; since the onExportState
arguments are not configurable, there are no arguments for exportState
.
Using FloodgateContext
Starting in v0.6.0
, Floodgate provides a named export FloodgateContext
that affords the use of the React Context API.
The FloodgateContext
's Consumer
component exposes the same object argument as the Floodgate#render
function.
Usage
This FloodgateContext
object can be used anywhere in the render prop function of a Floodgate
instance.
First, define a component that uses the Consumer
component:
// DeepChildControls.js
import { FloodgateContext } from "react-floodgate";
const DeepChildControls = (props) => {
return (
<div>
<FloodgateContext.Consumer>
{({ loadNext, loadAll, reset }) => (
<React.Fragment>
<button onClick={loadNext}>Load More</button>
<button onClick={loadAll}>Load All</button>
<button onClick={reset}>Reset</button>
</React.Fragment>
)}
</FloodgateContext.Consumer>
</div>
)
}
Then, import and use this component under a Floodgate render prop:
// LoadMoreArticles.js
import Floodgate from "react-floodgate";
import DeepChildControls from "./DeepChildControls";
export default function LoadMoreArticles(props) {
return (
<Floodgate data={props.data} initial={5} increment={5}>
{({ items }) => (
<div>
<h3>Articles</h3>
<section>
{items.map((story) => (
<article>
<h4>{story.title}</h4>
<p>{story.excerpt}</p>
</article>
))}
<footer>
{/* Use DeepChildControls here */}
<DeepChildControls />
</footer>
</section>
</div>
)}
</Floodgate>
)
}
Examples
Codesandbox Examples
- Floodgate basics
- Parent-controlled Floodgate
- Floodgate with Context API
- Floodgate with async data polling
Older Examples:
Contributors
Creating Issues
Request a documentation update
Setup for Development
- Clone/fork this repository
- Install dependencies using yarn or npm.
- Run any of the following commands: