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 will assume the following for this tutorial:
package.json
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:
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.
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.
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 = () => {};
Our determineState
function is the heart of our component. It will have the logic for determining if we reached the bottom of the page:
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.
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.
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:
This ensures our observer fires on all possible parts that might cause a height change.
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);
}
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.
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>
)
}
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>
)
}
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>
)
}
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>
)
}
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>
)
}
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
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.
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.
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);
}
};
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 :)
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 ?
It's been a long ride! Let's get to publishing!
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:
require
s a packagenpm search
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.
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.
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
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.
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 ?
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.
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!