npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

react-nested-loader

v2.2.1

Published

Easiest way to inject a loader/spinner into a deeply nested component (like a button)

Downloads

547

Readme

React Nested Loader

  • Manage loading/error state of nested views/buttons triggering async actions
  • Not an UI lib, you provide the UI. Works with ReactNative.
  • No boilerplate at all, no need to use setState/Redux

Main usecase: button triggering api calls

You have a submit button on your form. For good UX you may want to:

  • show temporarily a spinner into the button
  • disable the button during the async operation
  • make the button blink on api errors

Unfortunately, you are using Redux/setState/whatever, and implementing this kind of UX detail takes too much time/pollutes state/creates boilerplate so it is left for later while it doesn't have to.

image image image image

Demo

Here is a CodeSandbox demo

Usage

npm install react-nested-loader

1) Create a button:


const Button = ({
  onClick, 
  loading,
  error,
}) => (
  <button onClick={onClick} disabled={loading}>
    {error ? "Error" : loading ? "..." : "Click me "}
  </button>
);

The button UI should be able to display appropriately loading/error states. You define the styling entirely.

2) Wrap your button:

import ReactNestedLoader from "react-nested-loader";

const LoadingButton = ReactNestedLoader({
  // optional but convenient: only inject the error for 1sec for blinking effect
  onError: (error, remove) => setTimeout(remove,1000), 
})(Button);

The ReactNestedLoader HOC will by default inject a loading=false and error=undefined prop to the wrapped component.

3) Return a promise in container/smartComp/controller:

const SomeIntermediateComp = ({onButtonClick}) => (
  <WhateverYouWant>
    <LoadingButton onClick={onButtonClick}/>
  </WhateverYouWant>
);

class Container extends React.Component {
  handleClick = () => {
    const promise = MyAPI.doSomethingAsync();
    // VERY IMPORTANT: the promise MUST be returned to the button (or you can use "async handleClick")
    return promise;
  };
  render() {
    return (
      <WhateverYouWant>
        <SomeIntermediateComp onButtonClick={this.handleClick}/>
      </WhateverYouWant>
    )
  }
}

Using the LoadingButton into a top-level component.

No need to use any local state, you just need to return the promise to the button, and the loading / error prop of your button will be automatically updated according to the state of the last intercepted promise.

API

const LoadingButton = ReactNestedLoader(Button);

Or

const LoadingButton = ReactNestedLoader(config)(Button);

Options

const DefaultConfig = {
  // The "loading" prop to use for injecting the loading boolean value
  loadingProp: "loading",

  // The "error" prop to use for injecting the rejection error on failed async operation
  errorProp: "error",

  // The "success" prop to use for injecting the success boolean on successful async operation
  successProp: false,

  // The "api" prop that will be injected into your component for manual control
  apiProp: false,


  // You might want to log the intercepted errors?
  // Sometimes you want to only display the promise error temporarily (for example, make the button blink on error)
  // You can do so with: onError: (error, remove, isCurrentPromise) => setTimeout(remove,1000)
  onError: (error, remove, isCurrentPromise) => {},

  // You can also inject a success boolean prop, and schedule its removal to give user feedback (like congratulations)
  onSuccess: (result, remove, isCurrentPromise) => {},

  // It is safer to delay by default slightly the loader removal
  // For example if your promise has 2 then() callbacks (removal of a view and loader removal),
  // this ensures that the loader is not removed just before view removal, leading to flicker
  delay: true,

  // Should we use React.forwardRef (meaning it won't be possible to get this comp instance, just the wrapped comp)
  forwardRef: true,

  // To which prop should the ref be forwarded
  // - if wrapped component use forwardRef, then "ref" makes sense
  // - else you may want to get the instance of the wrapped component, or it probably expose an "innerRef" prop...
  refProp: 'ref',
};

Features

  • Works with React and React-Native
  • The callback proxies are cached appropriately so that the underlying button does not render unnecessarily. If you provide stable callbacks, the HOC will pass-down stable proxies and your pure component button can bypass rendering
  • Will only handle the loading state of the last returned promise, to avoid concurrency issues (think takeLatest of Redux-saga`)
  • API injected as prop into button (props.reactNestedLoader.handlePromise(promise))
  • Can use React.forwardRef() (2.*)
  • Imperative API, when not forwarding ref (componentRef.api.handlePromise(promise))

Limits

The HOC does hold the button loading state as React component state. This means it won't be in your state management system (Redux/Apollo/Mobx...) and as any local state you will loose ability to use devtools to replay that state (or other fancy features). In my opinion it is not critical state that is worth putting in your Redux store anyway. I assume perfectly using this lib as well as Redux/Redux-saga/Apollo mutations.

Currently the lib only support injecting a single loading prop. As a component may receive multiple callbacks, we could inject multiple loading props. Please open issues with your specific usecase if needed.

Advices

  • Wrap generic app button with ReactNestedLoader and manage the loading prop inside it to show some alternative content like a spinner
  • When button component change from loading=false to loading=true, make sure the component dimension is not affected for better UX
  • A nice UX is to make the text disappear and make the spinner appear, as it does not mess-up with button dimensions (make sure to use a small-enough spinner)
  • If needed, pass spinner size in button props

TODOS

  • Find more explicit name?
  • Support more advanced usecases?
  • Tests

Hire a freelance expert

Looking for a React/ReactNative freelance expert with more than 5 years production experience? Contact me from my website or with Twitter.