react-floodgate

Configurable and flexible "load more" component for React

Usage no npm install needed!

<script type="module">
  import reactFloodgate from 'https://cdn.skypack.dev/react-floodgate';
</script>

README

react-floodgate 🌊

Configurable and flexible "load more" component for React


npm version GitHub release npm downloads npm license

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: 'jane@doe.com' }, 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

Older Examples:

Contributors

Creating Issues

Request a feature

Request maintenance

Request a documentation update

Setup for Development

  1. Clone/fork this repository
  2. Install dependencies using yarn or npm.
  3. Run any of the following commands:
    • npm run start: Starts the Rollup watch script for building from /src
    • npm run storybook: Starts the Storybook development environment
    • npm run test: Runs Jest tests once
    • npm run test:watch: Same as test, but sets up Jest and watches for changes

LICENSE

MIT