EM Business Products Corp.
  • Services
  • Case studies
  • Blog
  • Connect with us
EM Business Products Corp.
  • Services
  • Case studies
  • Blog

05/10/2018

How to Create an Open Source React Component

#react
How to Create an Open Source React Component

React Sticky Footer

I wanted to share my experience with developing a reusable open source React component, in this case, a sticky footer. I will explain the details on how to develop a component like this, and I will also explain how to get it published to the NPM registry so you may learn how to provide easy distribution into your projects. Let's get started!

I'm bringing sticky back

Prerequisites

I will assume the following for this tutorial:

  • You have experience developing with Node as a local dev environment
  • You have experience with NPM and defining dependencies via package.json
  • You have some basic experience with React

Component Design Considerations

Before going off into a fun coding frenzy in our IDE, we should think about how we want our component to behave. This may seem trivial, but planning ahead, even with a short list that outlines the behavior we want, can be extremely helpful. We should put ourselves in the shoes of the developers who will use your component to get an idea of what we should incorporate into the component.

Here are the features I wanted to see:

  • The sticky footer should display when the content is taller than the browser height, and the user has not reached the bottom of the page
  • When reaching the bottom of page, the sticky footer should hide, and the standard footer should now display in the normal document flow, as defined by the markup
  • The sticky footer and the standard footer may differ in style

Preparing for Development

NWB

I loved working with NWB while developing this library. It is a toolkit for creating React apps (among other apps), as well as React components. It is extremely simple to use and gets your local dev environment, and repository, all set up nicely, and it even has a demo app which you can use to test your component. Follow the instructions on developing React components with NWB to get your project generated locally, then let's continue with developing.

With your NWB project set up, you may simply run yarn start or npm start. As the info from the above link will tell you, this will run the demo application which you can use to develop your component in a React environment.

Implementation Details

Let's think about more technical considerations for our component.

We need a way to know when the user is scrolling down the window, so that we can check how far we have scrolled down the page as the scroll moves down. For this, we will listen for the window object's "scroll" event.

The "scroll" event will help us as we scroll, but if the content itself changes height without scrolling, this event will not trigger. So we will need a way to check if the sticky footer should show or hide, when the content's height changes. For this we will use MutationObserver. MutationObserver allows you to react to modifications made to the DOM.

Thinking about flexibility, we should try to give the consumer of the component ways to control exactly when the sticky footer hides and shows. We will add properties that give us control of the threshold for showing/hiding the sticky footer.

The consumer may also want a way to tell if the sticky footer is displaying or not. We should add a callback function for this.

We will also provide separate props for providing styles to the sticky footer and standard footer separately.

It should be noted that the design above is not without its flaws. As part of developing a component, it doesn't have to be perfect from the start. It can continue to evolve and improve as we put more time into it, and for now we are really just working on our first version :) It's also beneficial not to get too ahead of ourselves in features, and to let the developers communicate with us on what they would like to see, so we are not creating features unnecessarily.

The Fun Part: Building the Component

With our local development environment set up, and our design plan in place, let's get going with developing our component. In the src folder of your NWB generated project, add a blank index.js file.

Let's write the component skeleton

import React, { Component } from "react";
import PropTypes from "prop-types";

export default class StickyFooter extends Component {
  constructor(props) {
    super(props);
  }

  componentDidMount() {}

  componentWillUnmount() {}

  render() {}
}

We will make use of our component's state object to set whether we have reached the bottom of the page. Let's start by simply adding the initial state.

constructor(props) {
    super(props);
    this.state = {
        isAtBottom: false
    };
}

Now let's create a function that will be in charge of updating isAtBottom as needed. We will call this function determineState

determineState = () => {};

determineState Logic

Our determineState function is the heart of our component. It will have the logic for determining if we reached the bottom of the page:

  • Determine scroll value of the spot of the page shown at the bottom of the window
  • If this value is greater or equal to the total height of the document body, then we are at the bottom
  • If this value is not greater or equal to the total height of the document body, then we are not yet at the bottom

Creating determineState

Let's start with the first line that will simply store the value of the page scroll's bottom edge:

determineState = () => {
  const scrollOffset = window.pageYOffset + window.innerHeight;
};

Here we use the window object's pageYOffset to get the page's scroll position value. Since this value gives us the page's position at the top edge of the browser's viewport -- the area that draws your web page, we add the window's innerHeight to it, to know the bottom edge. innerHeight gives us the the height of the browser's viewport.

Our next line we will store our page content's height, by using document.body.clientHeight:

determineState = () => {
  const scrollOffset = window.pageYOffset + window.innerHeight;
  const contentHeight = document.body.clientHeight;
};

Next, we need to make use of these variables to check where we are on the page, and to modify our state's isAtBottom property, depending on the value we get.

determineState = () => {
  const scrollOffset = window.pageYOffset + window.innerHeight;
  const contentHeight = document.body.clientHeight;

  if (scrollOffset >= contentHeight) {
    this.setState({ isAtBottom: true });
  }
};

In the above code we check if scrollOffset (the page's scroll position at the bottom edge of the browser viewport) is greater or equal than the actual content's height. If so, we have reached the bottom and we set our isAtBottom state property to true.

Let's continue to add to our determineState function, by now adding an else if to set the state back to isAtBottom: false when appropriate.

determineState = () => {
  const scrollOffset = window.pageYOffset + window.innerHeight;
  const contentHeight = document.body.clientHeight;

  if (scrollOffset >= contentHeight) {
    this.setState({ isAtBottom: true });
  } else if (scrollOffset < contentHeight - contentHeight) {
    this.setState({ isAtBottom: false });
  }
};

In the else if, we check if scrollOffset is less than our content height. If so, we know we have not yet reached the bottom of the page, so we set isAtBottom to false.

Scroll Event

Let's hook up our determineState function to run whenever the user is scrolling the page. We will add a listener for the "scroll" event as soon as the component mounts:

componentDidMount() {
    window.addEventListener("scroll", this.handleScroll);
}

Let's create the handleScroll function and call determineState inside of it

handleScroll = () => {
  this.determineState();
};

We can now show/hide the sticky footer when needed as we scroll.

MutationObserver

Let's hook up MutationObserver to invoke our determineState function when a mutation is detected.

componentDidMount() {
    this.observer = new MutationObserver(mutations => {
      this.determineState();
    });
    this.observer.observe(document.body, {
      childList: true,
      subtree: true,
      attributes: true
    });
    window.addEventListener("scroll", this.handleScroll);
    this.determineState();
  }

Above, we create an instance of MutationObserver and store it into our observer variable. Within the callback, we simply call determineState.

When we call the observer method, we pass some options to it:

  • childList: true = This observes additions/removals of child elements of our target (document.body)
  • subtree: true = Observes mutations to our target's descendants
  • attributes: true = Observes changes to our target's attributes

This ensures our observer fires on all possible parts that might cause a height change.

Cleaning Up

MutationObserver requires that we stop observing when we are done. We also should remove the "scroll" event listener when the component unmounts

componentWillUnmount() {
    this.observer.disconnect();
    window.removeEventListener("scroll", this.handleScroll);
}

Rendering The Content

Let's actually let our component render something to our screen now :) We don't want to style the footer ourselves in a specific way because our component would not be reusable if we did, or at least it would be harder to style. We want the developer to provide their own custom footer and have full control of it. We will add basic styling, and simply pass the footer as a child element into our sticky footer.

Add Markup

Let's add our basic markup. I will use a div to keep things generic, and again, give the developer the freedom to choose an appropriate tag (like <footer>, or some other tag, or even another React component)

render() {
    return (
        <div></div>
    )
}
Pass Children Through

First thing we should do is pass the children that will be provided by the developer, through into our render method

render() {
    return (
        <div>
            <div>{this.props.children}</div>
        </div>
    )
}
Add Sticky Style

We are in control of making the footer sticky, let's add a simple style for that

render() {
    let fixedStyles = {
      position: 'fixed',
      bottom: 0
    };
    return (
        <div>
            <div style={fixedStyles}>{this.props.children}</div>
        </div>
    )
}
Render Sticky Footer Conditionally

We should not render the sticky styled footer always of course. It would defeat the purpose of our sticky footer component. Let's use our isAtBottom state value so it renders only if the state isAtBottom is false

render() {
    let fixedStyles = {
      position: 'fixed',
      bottom: 0
    };
    return (
        <div>
            {!this.state.isAtBottom && <div style={fixedStyles}>{this.props.children}</div>}
        </div>
    )
}
Render Standard Footer Conditionally

We are now rendering the sticky footer, but we will render nothing if we reach the bottom. We will render the standard footer always. It simply won't be in our view if we are scrolled up:

render() {
    let fixedStyles = {
      position: 'fixed',
      bottom: 0
    };
    return (
        <div>
            <div>{this.props.children}</div>
            {!this.state.isAtBottom && <div style={fixedStyles}>{this.props.children}</div>}
        </div>
    )
}

Add Further Control

Woohoo, we have a working component :D Let's show our developers extra love by giving them some more control over the sticky footer behavior

Threshold Control for Hiding the Sticky Footer

In some cases the developer may want to adjust the moment when the sticky footer hides before it reaches the bottom. Let's add a bottomThreshold prop to help us control this. At the very end of the file, after the closing bracket of the class declaration, let's add a propTypes property, and define bottomThreshold to help React check that valid props are provided.

StickyFooter.propTypes = {
  bottomThreshold: PropTypes.number,
};

Let's add a default value for this threshold. At the end of the file, below our propTypes, let's add a defaultProps setting, and make our default for the threshold prop to 0:

StickyFooter.defaultProps = {
  bottomThreshold: 0,
};

Now let's make use of the prop. We will use this in the determineState method:

determineState = () => {
  const scrollOffset = window.pageYOffset + window.innerHeight;
  const contentHeight = document.body.clientHeight - this.props.bottomThreshold;

  if (scrollOffset >= contentHeight) {
    this.setState({ isAtBottom: true });
  } else if (scrollOffset < contentHeight - contentHeight) {
    this.setState({ isAtBottom: false });
  }
};

This threshold value basically allows you to tell your component how much height to remove from your page content. A positive number will make your sticky footer hide sooner, since the component will think we reached the bottom sooner. A negative number probably doesn't make sense here, since your actual page content will stop scrolling before reaching that value, making your sticky footer show no matter what.

Threshold Control for Showing the Sticky Footer

Another handy control can be added to let developers show the sticky footer at a threshold different from the one used to hide the footer. For this let's add another prop, called stickAtThreshold, with a default value of 0.001:

StickyFooter.propTypes = {
  bottomThreshold: PropTypes.number,
  stickAtMultiplier: PropTypes.number,
};

StickyFooter.defaultProps = {
  bottomThreshold: 0,
  stickAtThreshold: 0.001,
};

The value of 0.001 felt like a good default value for me. Feel free to change it to something else.

Let's make use of this threshold in, you guessed it, the determineState function :)

determineState = () => {
  const scrollOffset = window.pageYOffset + window.innerHeight;
  const contentHeight = document.body.clientHeight - this.props.bottomThreshold;

  if (scrollOffset >= contentHeight) {
    this.setState({ isAtBottom: true });
  } else if (
    scrollOffset <
    contentHeight - contentHeight * this.props.stickAtThreshold
  ) {
    this.setState({ isAtBottom: false });
  }
};

This threshold is created by using a percentage of the content height (the 0.001) as the bottom boundary to indicate to the component that the sticky footer should display again.

Display Callback

Continuing on here with our developer love-giving, let's give them a handy callback function that can tell them whether we have reached our bottom threshold or not.

Let's add our function as a prop:

StickyFooter.propTypes = {
  bottomThreshold: PropTypes.number,
  stickAtMultiplier: PropTypes.number,
  onFooterStateChange: PropTypes.func,
};

We will call this function whenever our isAtBottom state changes:

determineState = () => {
  const scrollOffset = window.pageYOffset + window.innerHeight;
  const contentHeight = document.body.clientHeight - this.props.bottomThreshold;

  if (scrollOffset >= contentHeight) {
    this.setState({ isAtBottom: true });

    this.props.onFooterStateChange && this.props.onFooterStateChange(true);
  } else if (
    scrollOffset <
    contentHeight - contentHeight * this.props.stickAtThreshold
  ) {
    this.setState({ isAtBottom: false });

    this.props.onFooterStateChange && this.props.onFooterStateChange(false);
  }
};
Custom Styling

For our last feature, the developer may want to apply a different style on the sticky footer, and on the standard footer. For this, lets provide two style props that will be fed to their corresponding footer type. We will call these props stickyStyles and normalStyles, with an empty object as default:

StickyFooter.propTypes = {
  bottomThreshold: PropTypes.number,
  stickAtThreshold: PropTypes.number,
  stickyStyles: PropTypes.object,
  normalStyles: PropTypes.object,
  onFooterStateChange: PropTypes.func,
};

StickyFooter.defaultProps = {
  bottomThreshold: 0,
  stickAtThreshold: 0.001,
  stickyStyles: {},
  normalStyles: {},
};

Let's make use of these in our markup:

render() {
    let fixedStyles = {
            ...this.props.stickyStyles,
      position: 'fixed',
      bottom: 0
    };
    return (
        <div>
            <div style={this.props.normalStyles}>{this.props.children}</div>
            {!this.state.isAtBottom && <div style={fixedStyles}>{this.props.children}</div>}
        </div>
    )
}

That's it for the styles! Let's add a final touch to help our developers -- documentation :)

Add some documentation

Nobody wants to guess at how your component works. Keep developers happy with some easy to understand documentation. Here is what I wrote for my propTypes:

StickyFooter.propTypes = {
  /**
   * A value that tells the component how close to the bottom should the scroller be before the sticky footer hides
   * and displays at the end of your content. The default is 0, meaning the user needs to scroll all the way to the bottom
   * before the footer hides. A number greater than 0 would cause the sticky footer to hide at some point before the user
   * has scrolled all the way down, depending on the value of the number.
   */
  bottomThreshold: PropTypes.number,
  /**
   * A value that tells the component how much the user should scroll back up before the sticky footer shows up again.
   * The default is 0.001. A number greater than the default would require the user scroll up more before the
   * sticky footer shows up.
   */
  stickAtThreshold: PropTypes.number,
  /**
   * Styles to be applied to the sticky footer only.
   */
  stickyStyles: PropTypes.object,
  /**
   * Styles to be applied to the footer in its standard location only.
   */
  normalStyles: PropTypes.object,
  /**
   * Callback that informs when the state of the footer has changed from sticky to being in normal document flow, via boolean argument.
   * true means it is in normal flow, false means it is sticky.
   */
  onFooterStateChange: PropTypes.func,
};

Aside from code documentation, you should also add some to your README.md file. A few examples of how to use your component will help too ?

Preparing for Publishing

It's been a long ride! Let's get to publishing!

package.json

NWB creates a package.json for you, which you should modify for the purposes of your open source component. Here is my package.json file for this project:

{
  "name": "react-sticky-footer",
  "version": "0.1.0-rc2",
  "description": "A simple sticky footer component for your React apps",
  "main": "lib/index.js",
  "jsnext:main": "es/index.js",
  "module": "es/index.js",
  "files": ["css", "es", "lib", "umd"],
  "scripts": {
    "build": "nwb build-react-component",
    "clean": "nwb clean-module && nwb clean-demo",
    "start": "nwb serve-react-demo",
    "test": "nwb test-react",
    "test:coverage": "nwb test-react --coverage",
    "test:watch": "nwb test-react --server"
  },
  "dependencies": {},
  "peerDependencies": {
    "react": "16.x"
  },
  "devDependencies": {
    "nwb": "0.21.x",
    "react": "^16.3.2",
    "react-dom": "^16.3.2"
  },
  "author": "Daniel Montano <[email protected]>",
  "homepage": "https://droplet.embusinessproducts.com",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/embp/react-sticky-footer/issues"
  },
  "repository": "https://github.com/embp/react-sticky-footer.git",
  "keywords": [
    "react",
    "component",
    "sticky footer",
    "footer",
    "react-sticky-footer"
  ]
}

I will break down the fields in the package.json file:

  • name: The name of your project
  • version: The version of your component. When you make changes to your component and want to publish a new version, this version must be incremented. I recommend you use semantic versioning as it is very easy to use and NPM friendly
  • description: A description of what your component does
  • main: The main JS entry point, used when a client requires a package
  • module: The main JS entry point for the ES6 version of your component (this is configurable through NWB)
  • jsnext:main: Same as module, but it is a non-standard property which may be used in some cases. (Here is some more detailed info on these entry point properties)
  • files: Files that will be included with the NPM publish of this component
  • scripts: Scripts related to development of this project
  • dependencies: Dependencies of this component that will be installed when installing this component
  • peerDependencies: Dependencies that need to be installed along with this component
  • devDependencies: Development dependencies associated with the project. These should be installed manually by the developer using your project
  • author: Email of the author of the project
  • homepage: The project's homepage
  • license: The license (if any) to apply for the use of your component
  • bugs: URL where a developer can post issues on your component
  • repository: The repository url for your component project
  • keywords: Keywords to provide to help people find your project when running npm search

Update the README.md file

NWB gives you a template README.md file you can use to describe your project, provide examples or documentation. Be sure to replace the generated content with something specific to your component.

Create an NPM account

Before you can publish to the NPM registry, you will need to create an account with NPM. Once you have the account ready, you will need to add this account to your local NPM:

npm adduser

The adduser command will prompt you for the user name and password you created for NPM. With this in place you will be ready to publish after completing the remaining steps :) You will not need to be in any specific folder, as this user will be applied globally to NPM.

Build the project for production

NWB provides a command that will build your component for a production release. From the root of your project simply issue the following command:

yarn build

or

npm run build

Tag the version in your repo

With your project now ready to publish, give your last commit a tag to identify this release. A typical tag would be your component's version number. Here's an example:

git tag v0.0.1

This will tag with v0.0.1 at the current commit. If you'd like to choose a specific commit, you may add the commit hash you want to target at the end, for example:

git tag v0.0.1 9fceb02

Once you have tagged, you will need to push the tag to the remote repo:

git push origin v0.0.1

Tagging will keep you organized as you continue to develop your component. You will always know the commit of a specific version, should you have a need to go back to it.

Run the NPM publish

We are finally ready to publish! NPM makes this extremely simple. Make sure you are in your project root folder and issue the following command:

npm publish

That's it ?

Making updates after publishing

Once your first version is out you will most likely make changes to it to continue to improve it. The first thing you will want to do is update the version number in package.json. NPM will not allow you to publish a component without a new version number.

If the version you'd like to publish is still in a testing phase, a useful approach for testing new versions out, without setting them as official, can be to use the beta tag when publishing.

Publishing a beta release

To publish with the beta tag you can issue this command:

npm publish --tag beta

With this, your component will not be under latest in NPM, and will be available through as beta release. For example, if a developer wanted to install your component via npm install your-component, this by default installs from the latest tag, the latest official release of your component. This let's you continue to release versions that may be unstable and may require further testing through beta, and can become an official release once they are ready.

A consuming developer would install the beta version by explicitly setting the beta tag:

yarn add your-component@beta

or

npm install your-component@beta

To make a beta release an official release, simply publish the component (with an updated version number) as before:

npm publish

I hope this post has been informative, and will give you some understanding on creating your own custom components.

Here's my version of the sticky footer. Let me know what you think :)

Happy coding!

Like this article? Cool! You may like these:

CONNECT WITH US

Full Name

Email address

Message

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.