Skip to content

nucleuslabs/react-ajax-loader

Repository files navigation

react-ajax-loader

Higher-order component for loading data into your React components. Supports batching, deduping and caching requests.

Installation

yarn add react-ajax-loader

Usage

Create a file that exports an instance of AjaxLoader.

// ajax-loader.js
import {AjaxLoader, MemoryCache} from 'react-ajax-loader';

export default new AjaxLoader({
    endpoint: '/path/to/your/special/endpoint.php',
    cache: new MemoryCache(), // <-- Cache responses in memory. Use `null` to *not* cache, or write your own to save data to `localStorage` or whatever you want.
    batchSize: 5, // Send up to 5 requests at once
    minDelay: 10, // Wait at least 10ms for more requests to come in (when components are mounted)
    maxDelay: 50, // Wait at most 50ms before sending the batch
    defaultDataProp: 'data', // Put successful requests into "data" prop
    defaultLoadingProp: 'loading', // Put count of pending requests into "loading" prop
    defaultErrorProp: 'error', // Put errors here
    refreshAllProp: 'refresh', // Re-send all requests when this.props.refresh() is called (skip cache)
    fetchOptions: () => { // <-- Merged into window.fetch()
        if(window.csrfToken) {
            return {
                headers: {
                    'X-Csrf-Token': window.csrfToken, // Pass a CSRF token with every request if you need to
                }
            };
        }
        return {};
    }
});

Write a special endpoint to handle batch requests. Here's an example in PHP from our project, but you can use any server-side language:

<?php // special/endpoint.php

public static function handle(\NucleusLabs\JsonBag $batch) {
    $routeHandler = new \NucleusLabs\Routing\RouteHandler();

    $responses = [];

    $requests = $batch->get('requests');

    if(!$requests) {
        throw new \Exception("AjaxLoader batch request is missing 'requests'");
    }

    foreach($requests as $req) {
        try {
            $payload = $routeHandler->handleRoute($req['route'], $req['data']);
        } catch(\Exception $ex) {
            $responses[] = [
                'type' => 'error', // <-- Use this if the request failed
                'payload' => [ // <-- Payload can be whatever. It will be put into the "error" prop
                    'status' => $ex instanceof \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface ? $ex->getStatusCode() : null,
                    'message' => $ex->getMessage(),
                    'code' => $ex->getCode(),
                ],

            ];
            continue;
        }

        $newEtag = md5(json_encode($payload, JSON_UNESCAPED_SLASHES));
        $oldEtag = $req['etag'] ?? [];

        if($newEtag === $oldEtag) {
            $responses[] = [
                'type' => 'nochange', // <-- Not necessary to implement, but will make the response smaller when the data hasn't changed on the server
            ];
        } else {
            $responses[] = [
                'type' => 'success',
                'payload' => $payload, // <-- Whatever you want. Will be put into "data" prop
                'etag' => $newEtag, // <-- Hash of the payload. Will be returned back to when you use the "cache-and-network" fetchPolicy and the request is cached. Use to avoid sending same payload again.
            ];
        }
    }

    return [
        'rank' => $batch->get('rank'), // <-- Return the "rank" as-is. It's used to discard stale requests when you send multiple in quick succession.
        'responses' => $responses,
    ];
}

Use the HOC in all of your components:

// UserSelect.jsx
import ajaxLoader from './ajax-loader.js';
import pick from 'lodash/fp/pick';

const UserSelect = ajaxLoader.hoc({
    route: 'getUsers', // <-- Use to determine what function to call on the server
    data: pick(['programId']), // <-- Choose some props to pass along; ajax request will be re-sent whenever these change
})(({loading,data}) => {
    if(loading) {
        return <span>Loading...</span>;
    }
    
    return (
        <select>
            {data.map(opt => (
                <option key={opt.id} value={opt.id}>{opt.name}</option>
            ))}
        </select>
    );
});

API

AjaxLoader.constructor(options)

{
    endpoint,
    cache,
    hash = objectHash,
    batchSize = 4, 
    minDelay = 8, 
    maxDelay = 32, 
    fetchOptions,
    refreshAllProp,
    
    defaultDataProp = 'ajaxData',
    defaultLoadingProp = 'ajaxLoading',
    defaultErrorProp = 'ajaxError',
    defaultEqualityCheck = shallowEqual,
    defaultHandler = setStateHandler,
    defaultFetchPolicy = FP.CacheAndNetwork,
  
}) 

AjaxLoader.hoc(...requests)

{
    equalityCheck: this.options.defaultEqualityCheck, // Used to compare old request data to new to determine if request needs to be re-sent. Defaults to a shallow compare
    handler: this.options.defaultHandler, // Invoked when request was successful. Copies response into `props[dataProp]` by default. Override this if you have special requirements. Return an object with properties to set.
    loadingProp: this.options.defaultLoadingProp, // Name of loading prop
    errorProp: this.options.defaultErrorProp, // Name of error prop
    dataProp: this.options.defaultDataProp, // Name of data prop. Not used if you override handler.
    refreshProp: null, // Name of prop used to re-send just this request. Use `refreshAllProp` to re-send all requests bound to this component.
    fetchPolicy: this.options.defaultFetchPolicy, // One of "cache-first", "cache-and-network", "network-only" or "cache-only"
    initialData: null, // Bypass the first (on-mount) request and use this data instead. This allows you to pass the first payload with your HTML and avoid a RTT. Data will be passed through the `handler`
}

License

MIT.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published