File Or Image Uploads on Amazon Web Services ( AWS ) using React, Node and Express JS ( aws-sdk )

I will teach you the quickest and easiest way to upload single as well as multiple files on AWS using React js, Node js and Express js

Here are the steps you will take:
 1-Create an account on console.aws.amazon.com 
 2-Create a user and a new bucket. Note the accessKeyId and the secretAccessKey
 3-Add policy to the bucket and the user, add CORS to the bucket.
 4-Download the react-node-boilerplate github link ( link below )
 5-Install aws-sdk, multer-s3, multer, path npm modules
 6-Create form and api for file uploads.
 7-Use axios to send the request to your api for uploading file and you will get the filename and the url location of the file(s) uploaded on aws.

Tutorial Video:

Step-1: Create an account on console.aws.amazon.com

Step 2 and 3: To create bucket and user on AWS please follow my blog on below link and then come back to this blog.
https://medium.com/@imranhsayed/how-to-create-a-user-and-bucket-amazon-web-services-aws-40631416e65

Step-4: Download the react-node-boilerplate from the below github link ( or use your own ):
 https://github.com/imranhsayed/react-node-boilerplate

cd react-node-boilerplate
npm cache clean --force // in root dir
npm install
cd client
npm cache clean --force
npm install // in client directory
// to start the server
npm run dev

Step 5: Install the required npm modules

cd react-node-boilerplate
npm i aws-sdk multer-s3 multer path url --save

Step 6: Create a directory route, inside which create another directory api inside of which create a file profile.js and then write below code:

// route/api/profile.js
const express = require( 'express' );
const aws = require( 'aws-sdk' );
const multerS3 = require( 'multer-s3' );
const multer = require('multer');
const path = require( 'path' );
const url = require('url');
/**
* express.Router() creates modular, mountable route handlers
* A Router instance is a complete middleware and routing system; for this reason, it is often referred to as a “mini-app”.
*/
const router = express.Router();
/**
* PROFILE IMAGE STORING STARTS
*/
const s3 = new aws.S3({
accessKeyId: 'AKIAI4IYUCNFNIWHMB4Q',
secretAccessKey: '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',
Bucket: 'onclick'
});
/**
* Single Upload
*/
const profileImgUpload = multer({
storage: multerS3({
s3: s3,
bucket: 'onclick',
acl: 'public-read',
key: function (req, file, cb) {
cb(null, path.basename( file.originalname, path.extname( file.originalname ) ) + '-' + Date.now() + path.extname( file.originalname ) )
}
}),
limits:{ fileSize: 2000000 }, // In bytes: 2000000 bytes = 2 MB
fileFilter: function( req, file, cb ){
checkFileType( file, cb );
}
}).single('profileImage');
/**
* Check File Type
* @param file
* @param cb
* @return {*}
*/
function checkFileType( file, cb ){
// Allowed ext
const filetypes = /jpeg|jpg|png|gif/;
// Check ext
const extname = filetypes.test( path.extname( file.originalname ).toLowerCase());
// Check mime
const mimetype = filetypes.test( file.mimetype );
if( mimetype && extname ){
return cb( null, true );
} else {
cb( 'Error: Images Only!' );
}
}
/**
* @route POST api/profile/business-img-upload
* @desc Upload post image
* @access public
*/
router.post( '/profile-img-upload', ( req, res ) => {
profileImgUpload( req, res, ( error ) => {
// console.log( 'requestOkokok', req.file );
// console.log( 'error', error );
if( error ){
console.log( 'errors', error );
res.json( { error: error } );
} else {
// If File not found
if( req.file === undefined ){
console.log( 'Error: No File Selected!' );
res.json( 'Error: No File Selected' );
} else {
// If Success
const imageName = req.file.key;
const imageLocation = req.file.location;
// Save the file name into database into profile model
res.json( {
image: imageName,
location: imageLocation
} );
}
}
});
});
// End of single profile upload
/**
* BUSINESS GALLERY IMAGES
* MULTIPLE FILE UPLOADS
*/
// Multiple File Uploads ( max 4 )
const uploadsBusinessGallery = multer({
storage: multerS3({
s3: s3,
bucket: 'onclick',
acl: 'public-read',
key: function (req, file, cb) {
cb( null, path.basename( file.originalname, path.extname( file.originalname ) ) + '-' + Date.now() + path.extname( file.originalname ) )
}
}),
limits:{ fileSize: 2000000 }, // In bytes: 2000000 bytes = 2 MB
fileFilter: function( req, file, cb ){
checkFileType( file, cb );
}
}).array( 'galleryImage', 4 );
/**
* @route POST /api/profile/business-gallery-upload
* @desc Upload business Gallery images
* @access public
*/
router.post('/multiple-file-upload', ( req, res ) => {
uploadsBusinessGallery( req, res, ( error ) => {
console.log( 'files', req.files );
if( error ){
console.log( 'errors', error );
res.json( { error: error } );
} else {
// If File not found
if( req.files === undefined ){
console.log( 'Error: No File Selected!' );
res.json( 'Error: No File Selected' );
} else {
// If Success
let fileArray = req.files,
fileLocation;
const galleryImgLocationArray = [];
for ( let i = 0; i < fileArray.length; i++ ) {
fileLocation = fileArray[ i ].location;
console.log( 'filenm', fileLocation );
galleryImgLocationArray.push( fileLocation )
}
// Save the file name into database
res.json( {
filesArray: fileArray,
locationArray: galleryImgLocationArray
} );
}
}
});
});
// We export the router so that the server.js file can pick it up
module.exports = router;

Add this in server.js

const profile = require( './routes/api/profile' );
app.use( '/api/profile', profile );

In Home.js for single and multiple file upload add the below code

import React, { Component } from 'react';
import axios from 'axios';
import $ from 'jquery';
class Home extends Component {
constructor( props ) {
super( props );
this.state = {
selectedFile: null,
selectedFiles: null
}
}
singleFileChangedHandler = ( event ) => {
this.setState({
selectedFile: event.target.files[0]
});
};
multipleFileChangedHandler = (event) => {
this.setState({
selectedFiles: event.target.files
});
console.log( event.target.files );
};
singleFileUploadHandler = (  ) => {
const data = new FormData();
// If file selected
if ( this.state.selectedFile ) {
data.append( 'profileImage', this.state.selectedFile, this.state.selectedFile.name );
axios.post( '/api/profile/profile-img-upload', data, {
headers: {
'accept': 'application/json',
'Accept-Language': 'en-US,en;q=0.8',
'Content-Type': `multipart/form-data; boundary=${data._boundary}`,
}
})
.then( ( response ) => {
if ( 200 === response.status ) {
// If file size is larger than expected.
if( response.data.error ) {
if ( 'LIMIT_FILE_SIZE' === response.data.error.code ) {
this.ocShowAlert( 'Max size: 2MB', 'red' );
} else {
console.log( response.data );
// If not the given file type
this.ocShowAlert( response.data.error, 'red' );
}
} else {
// Success
let fileName = response.data;
console.log( 'fileName', fileName );
this.ocShowAlert( 'File Uploaded', '#3089cf' );
}
}
}).catch( ( error ) => {
// If another error
this.ocShowAlert( error, 'red' );
});
} else {
// if file not selected throw error
this.ocShowAlert( 'Please upload file', 'red' );
}
};
multipleFileUploadHandler = () => {
const data = new FormData();
let selectedFiles = this.state.selectedFiles;
// If file selected
if ( selectedFiles ) {
for ( let i = 0; i < selectedFiles.length; i++ ) {
data.append( 'galleryImage', selectedFiles[ i ], selectedFiles[ i ].name );
}
axios.post( '/api/profile/multiple-file-upload', data, {
headers: {
'accept': 'application/json',
'Accept-Language': 'en-US,en;q=0.8',
'Content-Type': `multipart/form-data; boundary=${data._boundary}`,
}
})
.then( ( response ) => {
console.log( 'res', response );
if ( 200 === response.status ) {
// If file size is larger than expected.
if( response.data.error ) {
if ( 'LIMIT_FILE_SIZE' === response.data.error.code ) {
this.ocShowAlert( 'Max size: 2MB', 'red' );
} else if ( 'LIMIT_UNEXPECTED_FILE' === response.data.error.code ){
this.ocShowAlert( 'Max 4 images allowed', 'red' );
} else {
// If not the given ile type
this.ocShowAlert( response.data.error, 'red' );
}
} else {
// Success
let fileName = response.data;
console.log( 'fileName', fileName );
this.ocShowAlert( 'File Uploaded', '#3089cf' );
}
}
}).catch( ( error ) => {
// If another error
this.ocShowAlert( error, 'red' );
});
} else {
// if file not selected throw error
this.ocShowAlert( 'Please upload file', 'red' );
}
};
// ShowAlert Function
ocShowAlert = ( message, background = '#3089cf' ) => {
let alertContainer = document.querySelector( '#oc-alert-container' ),
alertEl = document.createElement( 'div' ),
textNode = document.createTextNode( message );
alertEl.setAttribute( 'class', 'oc-alert-pop-up' );
$( alertEl ).css( 'background', background );
alertEl.appendChild( textNode );
alertContainer.appendChild( alertEl );
setTimeout( function () {
$( alertEl ).fadeOut( 'slow' );
$( alertEl ).remove();
}, 3000 );
};
render() {
return(
<div>
<div className="container">
{/* For Alert box*/}
<div id="oc-alert-container"></div>
{/* Single File Upload*/}
<div className="card border-light mb-3 mt-5" style={{ boxShadow: '0 5px 10px 2px rgba(195,192,192,.5)' }}>
<div className="card-header">
<h3 style={{ color: '#555', marginLeft: '12px' }}>Single Image Upload</h3>
<p className="text-muted" style={{ marginLeft: '12px' }}>Upload Size: 250px x 250px ( Max 2MB )</p>
</div>
<div className="card-body">
<p className="card-text">Please upload an image for your profile</p>
<input type="file" onChange={this.singleFileChangedHandler}/>
<div className="mt-5">
<button className="btn btn-info" onClick={this.singleFileUploadHandler}>Upload!</button>
</div>
</div>
</div>
{/* Multiple File Upload */}
<div className="card border-light mb-3" style={{ boxShadow: '0 5px 10px 2px rgba(195,192,192,.5)' }}>
<div className="card-header">
<h3 style={{ color: '#555', marginLeft: '12px' }}>Upload Muliple Images</h3>
<p className="text-muted" style={{ marginLeft: '12px' }}>Upload Size: 400px x 400px ( Max 2MB )</p>
</div>
<div className="card-body">
<p className="card-text">Please upload the Gallery Images for your gallery</p>
<input type="file" multiple onChange={this.multipleFileChangedHandler}/>
<div className="mt-5">
<button className="btn btn-info" onClick={this.multipleFileUploadHandler}>Upload!</button>
</div>
</div>
</div>
</div>
</div>
);
}
}
export default Home;

When you upload the pictures you will get the response in form of file name and location url which you can save into the database and then use that url to display the images on your project .. The images will be store on aws bucket of yours and the url will be used to display the individual images.

Single Image Response
Multiple image response

How to create a user and bucket Amazon Web Services ( AWS )

In this blog you will learn how to create amazon bucket and user and get the Access key ID and Secret access key for uploading files..

Step-1: Create an account on console.aws.amazon.com

Step-2: Create user on console.aws.amazon.com
Once you have created the account Click on service > Security, Identity & Compliance > IAM

Now click on Users > Add User

Enter a new username and select Access types as ‘Programmatic access’ .Then click on Next Permission > Next review > Create User

Note down your Access key ID and Secret access key and click on close.

Now Click on the user created e.g. orionbuckets

Note down the ARN : e.g. arn:aws:iam::883235560421:user/orionbuckets.
Then click on Add inline policy

Click on JSON and add the below policy code:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListAllMyBuckets",
"s3:PutObject",
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::*"
]
}
]
}

Then click on Review Policy , give it a name and Create Policy . Then Click on Services( from the Navmenu ) > S3

Then click on Create bucket

Enter a unique bucket name , select Region and then click on Next .Then Create bucket

Then click on the newly created bucket and then click on Permissions > Bucket Policy

Then add the below policy code by replacing the Resource value to the arnvalue you see on top and Principle AWS to ARN you copied when you created the user e.g. arn:aws:iam::883235560421:user/orionbuckets, and then click save

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AddCannedAcl",
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::883235560421:user/orionfileuploads"
]
},
"Action": [
"s3:PutObject",
"s3:PutObjectAcl"
],
"Resource": [
"arn:aws:s3:::onclick/*"
],
"Condition": {
"StringEquals": {
"s3:x-amz-acl": [
"public-read"
]
}
}
}
]
}

Now got CORS configuration and add this, then click save

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>POST</AllowedMethod>
<AllowedMethod>PUT</AllowedMethod>
<MaxAgeSeconds>3000</MaxAgeSeconds>
<AllowedHeader>Authorization</AllowedHeader>
</CORSRule>
</CORSConfiguration>

And its done!!

Interesting things about Node JS

Node JS is a runtime environment built on Chrome’s V8 engine.
It is is very efficient in communication. Its great with WebSocket communication.
Write data validation only once( DRY Rule )
Because you can write your JS code both for front end and for the server, you can do the data validation only one.

How does Node work?

Node is not just a JavaScript engine. You can take the V8 engine compile it and run it on your server and you can even put some JS code in it and it will do that code. But what you cannot do with V8 is that you cannot see what it does.
JS engine does not have any concept of I/O.
So what node does is, it provides a hosting environment for that JavaScript. It extends the JS from its pure native sense into an environment that lets you do stuff with it like write to the screen or open up and read a file.

Please not that when you do console.log in your browser, the browser and the developer tools have provided an I/O hook. that allows that console.log to work.
Node does a similar thing. Node has taken the I/O ( Input Output stream ). Its given you a binding from its lower level C layer into the JS so that when we do console.log in node we can actually see something printing to the screen.

Browser APIs

Storage API

Local Storage and Session Storage

Few Important points:

1-Local Storage never expire.
2. Session storage expire when the user closes the browser tab. Each browser tab will have a separate session.
3. When you change local Storage data, an event gets fired that you can listen to. If user has open multiple tabs and changes a data that is being stored on in local storage. It will get updated for other browser tabs as well.

Facades:

A facades is a thin layer of abstraction around a native API. It is not a polyfill. It does not fill or add any new functionality on top of the existing api.
Frameworks are a more complex version of Facade. They add polyfill etc.
You should not use the Web APIs like Canvas API directly, you should either use a facade or a framework.

WebSocket API

An ajax request , from client to server and then back to server to client takes a total of 800 ms roundtrip. So using Ajax for developing games or chats would be slow.

In a web socket, when we make the initial request, we keep the socket open which is listening. So next time we need an interchange of the data from server to client or vice versa , we don’t have to establish a new connection. Its a persistent socket.
Websocket header is smaller . Its only 8 bytes.So we are sending less data in the header. It makes it fast.
A websocket request could take upto 100 ms.
We listen for an event using .on()and then you send events back using .emit() .So in websocket you send event back and forth. Both server and client can listen and send events. You can give a custom name to the events. You can also send data along with events.
Broadcasting
If a server wants to send a message to many users who have open socket port , instead of sending it through individual multiple request , it can broadcast that message.
Broadcasting is done on the server and not on the client.

Live Search with React

In this blog, we will learn about how to create a live search input field, which fetches the result as the user types in the query.
Of all the results available for that query, we will only fetch 20 results. We will create a pagination for the user to navigate to the next or previous 20 results, until no further results are available.

We will achieve this by working on the following steps:

  1. Create a Search.js component
  2. Create a stylesheet Search.css
  3. Create an onChange Event Handler for Search Input
  4. Install axios and create fetchSearchResults()
  5. Call fetchSearchResults() on user’s query
  6. Create renderSearchResults() to show the results
  7. Add the loader, error message and styles

Pagination:
8. Initialize state for storing page information
9. Update state with pages information
10. Create handlePageClick() to handle page click
11. Create a component for Pagination links
12. Include PageNavigation Component into Search.js
13. Add styles for Navigation Links

API link: https://pixabay.com/api/docs/#api_search_images

Tutorial Video:
You can also watch the tutorial to understand in detail.
Part 1:

Part 2:

Step 1: Create a Search.js component

Create a component called Search.js inside the src/components directory.
Initialize the state, and define below properties:
query: to store user’s query.
results: to store our fetched data.
loading: initially set to false, which will help us show loader while the results are fetched.
message: To store any error message
Add an input inside label, wrapped in a container div, for user to enter his query

import React from 'react';

class Search extends  React.Component {

	constructor( props ) {
		super( props );

		this.state = {
			query: '',
                        results: {},
                        loading: false,
                        message: '',
		};

	}

	render() {
		return (
			<div className="container">
				{/*Heading*/}
				<h2 className="heading">Live Search: React Application</h2>

				{/*Search Input*/}
				<label className="search-label" htmlFor="search-input">
					<input
						type="text"
						value=""
						id="search-input"
						placeholder="Search..."
					/>
					<i className="fa fa-search search-icon"/>
				</label>
				
			</div>
			)
	}
}

export default Search;

Now import Search.js into App.js

import React from 'react';
import Search from "./components/Search";

class App extends React.Component {
	render() {
		return (
			<div>
				<Search/>
			</div>
		);
	}
}

export default App;

Include font awesome inside public/index.html

<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">

Step 2: Create a stylesheet Search.css

Create a stylesheet src/Searc.css. Add styles for the above elements

.heading {
	font-size: 30px;
	padding: 16px 0;
	color: #444;
	text-align: center;
}

/*Container*/
.container {
	margin: 36px auto;
	width: 100%;
	max-width: 800px;
}


.search-label {
	position: relative;
}

.container input {
	width: 100%;
	padding: 16px;
	font-size: 36px;
	font-style: italic;
	color: #444;
	box-sizing: border-box;
	outline: none;
}

.search-icon {
	position: absolute;
	top: -10px;
	font-size: 24px;
	color: #555;
	right: 18px;
}

Now import Style.css into Search.js

import '../Style.css';

So at present you app would look like this:

Step 3: Create an onChange Event Handler for Input

Create an onChange Event Handler function that takes that sets the value of the query property of state to what user entered in the input. We will set loading to true and message as empty which will nullify any message that was printed before.

handleOnInputChange = (event) => {
	const query = event.target.value;
            this.setState({ query, loading: true, message: ''  } );
};

Now add that event to the input.

<input
   type="text"
   value=""
   id="search-input"
   placeholder="Search..."
   onChange={this.handleOnInputChange}
/>

Step 4: Install axios and create fetchSearchResults()

Install and import axios

$ npm install axios

// Search.js
import axios from 'axios';

Now create a function called fetchSearchResults() , which takes two parameters :
updatedPageNumber: Page number to be served which will help us later when we do pagination
query: What user has entered in the search input field.
We then add the searchUrl for which we are using pixabay API, which gives us the images data.
As we don’t want to keep making API call, every time the user enters a character, we would initialize a variable called cancel in our constructor and cancel the previous request before making a new one. We will use CancelToken that axios provides us for this.
1. We cancel the previous request,
2. generate a new token and,
3. Pass this token as a the second parameter in axios.get()
4. We make a get request to Pixabay API and store the response in state, using setState(). We also set the loading to false, and the relevant message when we have received the response.
5. If we receive any error we set that in the message property of state.

constructor( props ) {
	super( props );

	this.state = {
		query: '',
		loading: false,
		message: '',
	};
	this.cancel = '';
}

/**
 * Fetch the search results and update the state with the result.
 *
 * @param {int} updatedPageNo Updated Page No.
 * @param {String} query Search Query.
 *
 */
fetchSearchResults = (updatedPageNo = '', query ) => {

	const pageNumber = updatedPageNo ? `&page=${updatedPageNo}` : '';

	// By default the limit of results is 20
	const searchUrl = `https://pixabay.com/api/?key=12413278-79b713c7e196c7a3defb5330e&q=${query}${pageNumber}`;

	if (this.cancel) {
		// Cancel the previous request before making a new request
		this.cancel.cancel();
	}
	// Create a new CancelToken
	this.cancel = axios.CancelToken.source();

	axios
		.get(searchUrl, {
			cancelToken: this.cancel.token,
		})
		.then((res) => {
			const resultNotFoundMsg = !res.data.hits.length
				? 'There are no more search results. Please try a new search.'
				: '';

			this.setState({
				results: res.data.hits,
				message: resultNotFoundMsg,
				loading: false,
			});
		})
		.catch((error) => {
			if (axios.isCancel(error) || error) {
				this.setState({
					loading: false,
					message: 'Failed to fetch results.Please check network',
				});
			}
		});
};

Step 5: Call fetchSearchResults() on user’s query

When the user types his query in the search box, we will :
1.Check if the user’s query string is empty , if it is set the result to empty object.
2.If its not empty, set the query in the state, set loading to false, message to empty string
3. Call the fetchSearchResults() which will take page number ( 1 for now ) and query,
4. Cancel the previous request, make a new token .
5. When the response is received fetch results and set the result in state, loading to false, and message if any.
6. Set the error message if error received.

handleOnInputChange = (event) => {
	const query = event.target.value;

	if ( ! query ) {
		this.setState({ query, results: {}, message: '' } );
	} else {
		this.setState({ query, loading: true, message: '' }, () => {
			this.fetchSearchResults(1, query);
		});
	}
};

Its important to note that we call fetchSearchResults() after we have set the query and loading to true, by calling it inside the call back function of setState() . This is because setState() is asynchrnous and does not guarantee that the values will be set immediately.

Step 6: Create renderSearchResults() to show the results

Lets create a renderSearchResults() to render the results of the query.
1. We pull the results out of state using ES6 Object restructuring.
2. Check if the results has data and . then loop their each item to display the images using ES6 array map function.

renderSearchResults = () => {
	const {results} = this.state;

	if (Object.keys(results).length && results.length) {
		return (
			<div className="results-container">
				{results.map((result) => {
					return (
						<a key={result.id} href={result.previewURL} className="result-items">
							<h6 className="image-username">{result.user}</h6>
							<div className="image-wrapper">
								<img className="image" src={result.previewURL} alt={result.user}/>
							</div>
						</a>
					);
				})}
			</div>
		);
	}
};

Now lets call this function inside render(). Also pull query from state and set that to the input value

render() {
	const { query } = this.state;
	return (
		<div className="container">
			{/*Heading*/}
			<h2 className="heading">Live Search: React Application</h2>

			{/*Search Input*/}
			<label className="search-label" htmlFor="search-input">
				<input
					type="text"
					value={query}
					id="search-input"
					placeholder="Search..."
					onChange={this.handleOnInputChange}
				/>
				<i className="fa fa-search search-icon"/>
			</label>

			{/*Result*/}
			{ this.renderSearchResults() }
		</div>
		)
}

Step 7: Add the loader, error message and styles

  1. Add loader.gif from Git repo
  2. Pull out message and loading from state using const {message, loading } = this.state;
  3. Import loader in Search.js using import Loader from '../loader.gif';
  4. Include the loader image, and error message inside render method of Search.js just beneath label.
{/*Error Message*/}
{ message && <p className="message">{message}</p> }

{/*Loader*/}
<img  src={Loader} className={`search-loading ${loading ? 'show' : 'hide' }`}  alt="loader"/>

Lets add some CSS to our results container inside Search.css

/* Results */
.results-container {
	display: flex;
	flex-flow: wrap;
}

.result-items {
	position: relative;
	padding: 16px;
	border: 1px solid #898989;
	margin: 16px;
	text-align: center;
	min-width: 200px;
	text-decoration: none;
	box-shadow: 2px 2px 2px #898989;
}

.image-username {
	color: coral;
	font-size: 18px;
	position: absolute;
	bottom: 0;
	right: 0;
	margin: 0;
	background: rgba(0, 0, 0, 0.8);
	padding: 5px 10px;

}

.image {
	width: 100%;
	height: 200px;
}

.image-wrapper {
	display: flex;
	align-items: center;
	box-sizing: border-box;
	height: 100%;
	justify-content: center;
}

/*Loader*/
.search-loading {
	position: absolute;
	left: 0;
	right: 0;
	margin: auto;
}

/*Show and hide*/
.show {
	display: inline-block;
}

.hide {
	display: none;
}

Now if we make a search , we get the results and the loader shows while its fetching the data like so :

Bravo !! 🙂
We have got our app working. Now all we have to do is add pagination.

Pagination

Step 8: Initialize state for storing page information

Add totalResults, totalPages and currentPageNo properties to state, to store these information

constructor( props ) {
	super( props );

	this.state = {
		query: '',
		results: {},
		error: '',
		message: '',
		loading: false,
		totalResults: 0,
		totalPages: 0,
		currentPageNo: 0,
	};
	this.cancel = '';
}

Step 9: Add getPageCount() to calculate the no of page

Create a function getPageCount() to calculate how many pages will exists based on the result we received. So for example if the result total is 61 and we are serving 20 results per page.
total = 61
denominator = 20
This function will return 4 pages. ( 20 each for 3 pages makes a total of 60 and the remaining 1 result will go on the 4th page )
Here Math.floor() converts the number passed to an integer.

// Search.js
/**
 * Get the Total Pages count.
 *
 * @param total
 * @param denominator Count of results per page
 * @return {number}
 */
getPagesCount = (total, denominator) => {
	const divisible = total % denominator === 0;
	const valueToBeAdded = divisible ? 0 : 1;
	return Math.floor(total / denominator) + valueToBeAdded;
};

Step 10: Update state with pages information

  1. Calculate the total results and store it in total
  2. Calculate total pages count using the getPagesCount() and store it in totalPagesCount
  3. Current Page no will be equal to the updated page no received in the parameter of fetchSearchResults()

Now update these information using setState() when we receive the response.

axios
	.get(searchUrl, {
		cancelToken: this.cancel.token,
	})
	.then((res) => {
		const total = res.data.total;
		const totalPagesCount = this.getPagesCount( total, 20 );
		const resultNotFoundMsg = !res.data.hits.length
			? 'There are no more search results. Please try a new search.'
			: '';

		this.setState({
			results: res.data.hits,
			totalResults: res.data.total,
			currentPageNo: updatedPageNo,
			totalPages: totalPagesCount,
			message: resultNotFoundMsg,
			loading: false,
		});
	})

Step 10: Create handlePageClick() to handle page click.

We will now create a function called handlePageClick. This will take a parameter type . We will call this function when the user clicks on Next or Previous button ( which we will create in a moment ) .
If prev is passed, it will add 1 to the current page number, else it would subtract one from the current page number.
Remember that on our initial query we are passing 1 as the updated page no. and setting the current page no to 1. So when the users clicks on Next the current page no would become 2(1+1).
Also we won’t show the the previous button if the current page value is 1

Now we check if loading is false (means the previous request is complete).
Then we call the fetchSearchResults() to get the new search results with the same query but new updated pageno.

/**
 * Fetch results according to the prev or next page requests.
 *
 * @param {String} type 'prev' or 'next'
 */
handlePageClick = (type) => {
	event.preventDefault();
	const updatedPageNo =
		      'prev' === type
			      ? this.state.currentPageNo - 1
			      : this.state.currentPageNo + 1;

	if (!this.state.loading) {
		this.setState({ loading: true, message: '' }, () => {
			// Fetch previous 20 Results
			this.fetchSearchResults(updatedPageNo, this.state.query);
		});
	}
};

Now lets pull out currentPage and totalPages and create constants showPrevLink and showNextLink to help us decide when to show the Prev or Next links.
So if the current page is greater than 1 means user is either on the second or high page, and we should show the Prev link.
If the current page is less than the no of total pages, that means we still have more pages to show and we can show Next link

const { query, loading, message, currentPageNo, totalPages } = this.state;


// showPrevLink will be false, when on the 1st page, hence Prev link be shown on 1st page.
const showPrevLink = 1 < currentPageNo;

// showNextLink will be false, when on the last page, hence Next link wont be shown last page.
const showNextLink = totalPages > currentPageNo;

Step 11: Create a component for Pagination links

Lets create a component called PageNavigation.js inside src/component directory. We will create two links one to navigate to Prev page and the other to the Next page. This component will receive props from the Search.js which we will define in a moment when we import this component there.
showPrevLink and showNextLink props will be boolean values and will decide whether to show these links or not.
-And handlePrevClick and handleNextClick functions will called when the Prev and Next links are clicked respectively , to show results of the relevant pages.
-We will grey out the color of the these links to show the user they are not clickable until their previous request is served, using the loading prop. Loading will be true while the request is being served.

import React from 'react';

export default (props) => {
	const {
		      showPrevLink,
		      showNextLink,
		      handlePrevClick,
		      handleNextClick,
		      loading,
	      } = props;

	return (
		<div className="nav-link-container">
			<a
				href="#"
				className={`
				nav-link 
				${ showPrevLink ? 'show' : 'hide'}
				${ loading ? 'greyed-out' : '' }
				`}
				onClick={handlePrevClick}
				>
				Prev
			</a>
			<a
				href="#"
				className={`
				nav-link
				${showNextLink ? 'show' : 'hide'}
				${ loading ? 'greyed-out' : '' }
				`}
				onClick={handleNextClick}
				>
				Next
			</a>
		</div>
	);
};

Step 12: Include PageNavigation Component into Search.js

  1. Lets import it into our Search.js using import PageNavigation from './PageNavigation';
  2. And add PageNavigation component one before and the other after this.renderSearchResults(). This is so that we have navigation available before and after search results and the user doesn’t have to scroll up or down to go the next page.
{/*Navigation Top*/}
<PageNavigation
	loading={loading}
	showPrevLink={showPrevLink}
	showNextLink={showNextLink}
	handlePrevClick={() => this.handlePageClick('prev')}
	handleNextClick={() => this.handlePageClick('next')}
/>

{/*Result*/}
{ this.renderSearchResults() }

{/*Navigation Bottom*/}
<PageNavigation
	loading={loading}
	showPrevLink={showPrevLink}
	showNextLink={showNextLink}
	handlePrevClick={() => this.handlePageClick('prev')}
	handleNextClick={() => this.handlePageClick('next')}
/>

Step 13: Add styles for Navigation Links

Lets add styles in Search.js for these navigation links

/*Nav Links*/
.nav-link-container {
	margin: 20px 0;
	display: flex;
	justify-content: flex-end;
}

.nav-link {
	color: #555;
	text-decoration: none;
	border: 1px solid #898989;
	padding: 10px 20px;
	margin-right: 10px;
}
.greyed-out {
	background: #f1f1f1;
	opacity: 0.5;
}

Now we should update our handelOnInputChange(), by setting totalResults, totalPages, and currentPageNo to zero when there query is empty

handleOnInputChange = (event) => {
	const query = event.target.value;

	if ( ! query ) {
		this.setState({ query, results: {}, totalResults: 0, totalPages: 0, currentPageNo: 0, message: '' } );
	} else {
		this.setState({ query, loading: true, message: '' }, () => {
			this.fetchSearchResults(1, query);
		});
	}
};

Awesome!! 🙂
Thats all guys we have our entire live search application ready with pagination.

If you liked my blog, please give a thumbs up and follow me on twitter

Git Repo: https://github.com/imranhsayed/react-workshop/tree/live-search-react/src

Set Up React App with WebPack and Babel

In this blog you will learn to set up a React Application:

  • Using create-react-app
  • Using Webpack, Webpack Dev Server and Babel from scratch

I am assuming that you have Node installed already on your system.

Using create-react-app

We will first install the create-react-app globally. Then we create a project using create-react-app command.

npm install -g create-react-app
create-react-app projectname
cd projectname
npm run start

And then all we have to do is just run npm run start . And that’s it. That’s all you have to do to set up your React application.

Using Webpack, Webpack Dev Server and Babel from scratch

The former was simple right. However, let’s learn how we can set up our React App from scratch.

Step 1: Create package.json file

cd ~
mkdir projectname
cd ~/projectname
// Creates package.json file
npm init --yes

Step 2: Install react and react-dom

npm i react react-dom

Step 3: Install Babel

Let’s install babel and the required presets and plugins.

npm i -D @babel/preset-react @babel/preset-env @babel/core babel-loader @babel/plugin-proposal-class-properties

@babel/preset-react is preset for react,
@babel/preset-env is a smart preset that allows you to use the latest JavaScript without needing to micromanage which syntax transforms are needed by your target environment(s).
@babel/core contains the core functionality of Babel.
babel-loader will be used by webpack to transpile Modern JS into the JS code that browsers can understand.
Since all browsers don’t understand javascript’s static class properties feature @babel/plugin-proposal-class-properties plugin transforms static class properties as well as properties declared with the property initializer syntax.

Step 4: Create a babel config file .babelrc

Here we tell babel to use @babel/preset-env target the last few versions of browsers and support for them. This will ensure that when the browser is updated it will stop transpiling of the old browser version and will transpile for the new one.
modules: false
 means hey babel! don’t do anything with the modules let webpack handle it.
We also tell webpack to use @babel/preset-react for React and @babel/plugin-proposal-class-properties to transform static class properties

{
"presets": [
[ "@babel/preset-env", {
"modules": false,
"targets": {
"browsers": [
"last 2 Chrome versions",
"last 2 Firefox versions",
"last 2 Safari versions",
"last 2 iOS versions",
"last 1 Android version",
"last 1 ChromeAndroid version",
"ie 11"
]
}
} ],
"@babel/preset-react"
],
"plugins": [ "@babel/plugin-proposal-class-properties" ]
}

Step 5: Install Webpack and Webpack Dev Server

npm i -D webpack webpack-cli webpack-dev-server html-webpack-plugin path

Step 6: Create directories and files for the project

Create directories calledsrc and public .Create our HTML file public/index.htm , entry filesrc/index.js and a component file src/App.jsinside of it.

mkdir src public
touch src/index.js src/App.js public/index.html

Step 7: Set up Webpack configuration file webpack.config.js

Here html-webpack-plugin will use your custom index.html that will be rendered by webpack-dev-server
Please note that if you don’t pass any param in new HTMLWebpackPlugin() , then thehtml-webpack-plugin plugin will generate an HTML5 file for you that includes all your webpack bundles in the body using script tags.

Also add the style loader, css loader and file-loader for styles and images. As webpack understands javascript so we need to convert the styles and images in javascript using these loaders

npm install style-loader css-loader file-loader

const HtmlWebPackPlugin = require( 'html-webpack-plugin' );
const path = require( 'path' );

module.exports = {
    context: __dirname,
    entry: './src/index.js',
    output: {
        path: path.resolve( __dirname, 'dist' ),
        filename: 'main.js',
        publicPath: '/',
    },
    devServer: {
        historyApiFallback: true
     },
    module: {
        rules: [
            {
                test: /\.js?$/,
                exclude: /node_module/,
                use: 'babel-loader'
            },
            {
                test: /\.css?$/,
                use: [ 'style-loader', 'css-loader' ]
            },
            {
                test: /\.(png|j?g|svg|gif)?$/,
                use: 'file-loader'
            }
        ]
    },
    plugins: [
        new HtmlWebPackPlugin({
            template: path.resolve( __dirname, 'public/index.html' ),
            filename: 'index.html'
        })
    ]
};

Notice that we have also passed historyApiFallbackto true and public path to ‘/’
What it does is that it redirect all server requests to /index.html which will download all the JS resources and allow React Router to take it from there.
If we don’t do this then when you add routes later using react-router or @reach/router and if you access a route like /dashboard , the browser will make a GET request to /dashboard which will fail, as you have no logic on the server to handle that request.

So publicPath allows you to specify the base path for all the assets within your application. historyAPIFallback will redirect 404s to /index.html.

Step 7: Create a React Component src/App.js

Create a class inside src/App.js and export it

import React from 'react';
class App extends React.Component {
render() {
return(
<div>
My App Component
</div>
);
}
}
export default App

Step 8: Create a div#root inside public/index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>React App</title>
</head>
<body>
<div id="root"></div>
<script type="text/javascript" src="main.js"></script></body>
</html>

Step 9: Insert App.js component into the DOM

Now let’s insert the App.js component into div with the id root that exists public/index.html file

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from "./App";

ReactDOM.render( <App/>, document.getElementById('root') );

Step 10: Add scripts in the package.json

"scripts": {
"webpack-dev-server": "webpack-dev-server",
"dev": "webpack-dev-server --mode=development",
"prod": "webpack --mode=production"
},

Now run the webpack dev server.

npm run dev

Handle Images in React using Webpack

We need to install url-loader and file-loader html-webpack-plugin npm packages first

npm install url-loader file-loader html-webpack-plugin

Now add this configuration to your webpack.config.js

const HtmlWebPackPlugin = require( 'html-webpack-plugin' );
const path = require( 'path' );
module.exports = {
	context: __dirname,
	entry: './src/index.js',
	output: {
		path: path.resolve( __dirname, 'dist' ),
		filename: 'main.js',
	},
	devServer: {
		historyApiFallback: true
	},
	module: {
		rules: [
			{
				test: /\.js$/,
				use: 'babel-loader',
			},
			{
				test: /\.css$/,
				use: ['style-loader', 'css-loader'],
			},
			{
				test: /\.(png|jp?g|svg)$/,
				use: [{
					loader: "file-loader",
					options: {
						name: '[name].[ext]',
						outputPath: 'images/',
						publicPath: 'images/'
					}
				}]
			}
		]
	},
	plugins: [
		new HtmlWebPackPlugin({
			template: path.resolve( __dirname, 'public/index.html' ),
			filename: 'index.html'
		})
	]
};

Now you can add an image inside src/images directory. Then import it as any name you like in this case ProfileIcon. And then pass it in the images source in your component.

import ProfileIcon from './images/profile-icon-min.png';

render() {
 <div>
  <img src={ProfileIcon} className="my-profile-icon" alt="profile icon"/>
 </div>
}

It will look like this in production.

So it will create an images directory in production and add the image profile-icon-min.png into it.

And the image will be dynamically added in the src attribute of img tag.

Attributes and Components in Gutenberg Blocks | RichText | BlockControls | AlignmentToolbar

In the previous blog, we learned about how edit and save functions work. They describe the structure of the block’s appearance. However, they only returned simple paragraphs. In this blog, we will learn about how the block’s structure can be changed when the user changes a block

State of a block:

To achieve dynamic change in the block’s structure when the user changes a block, state of a block is maintained through the editing session as a plain JavaScript object.

Every time block is updated the edit function is called.

Attributes:

To extract the JavaScript object again from the saved content of a post and reuse it we use block type’s attribute property.They provide a mechanism to map from the saved markup to a JavaScript representation of a block.

Attribute Sources:

Attribute Sources help you extract block attribute values from saved post content. They help you map from the saved markup to a JavaScript representation of a block. If no attribute source is specified, the attribute will be saved to (and read from) the block’s comment delimiter.

hpq: Attribute sources are a superset of functionality provided by hpq, a small library used to parse and query HTML markup into an object shape.

There are different types of attributes sources you can use. For example children, attribute, text, html, query, meta

Example of an attributes source: 'meta'

registerBlockType( 'myguten-block/test-block', {

attributes: {
content: {
type: 'array',
source: 'children',
selector: 'p'
},
},

When you define these attributes into registerBlockType(), it is passed to the edit() and save()

source: ‘children’ means it will look for the text inside of selector <p>
The way you use it into your edit function:

edit( props ) {
let { attributes , setAttributes, className } = props;
function onChange( event ) {
setAttributes( { author: event.target.value } );
}

return <p onChange={ onChange }/>{ attributes.content }</p>;
},

Once it’s saved into the database, it can be extracted using props.attributes

save: ( props ) => {
console.log( 'save-props', props );
return (
<p>{ props.attributes.content } </p>
);
}

Comment Delimiter

By storing data in HTML comments we would know that we wouldn’t break the rest of the HTML in the document, that browsers should ignore it, and that we could simplify our approach to parsing the document.

Let’s create an attribute,

registerBlockType( 'myguten-block/test-block', {
title: 'Basic Example',
icon: 'smiley',
category: 'layout',
attributes: {
contentStyle: {
type: 'object',
default: {
color: 'black',
textAlign: 'left'
}
}
},
}

When we don’t specify a source in the attribute contentStyle, the data gets saved in the comment delimiter. And it can be extracted in the save function using props.attributes.contentStyle .

Data saved in the database

Benefits of using Comment Delimiter

  • Whereas HTML attributes are complicated to parse properly, comments are quite easily described by a leading <!-- followed by anything except -- until the first -->. This simplicity and permissiveness means that the parser can be implemented in several ways without needing to understand HTML properly.
  • We can use JSON literals inside the comment.
  • These explicit boundaries also protect damage in a single block from bleeding into other blocks or tarnishing the entire document.

Gutenberg Components

Because many blocks share the same complex behaviors, reusable components are made available to simplify implementations of your block’s edit function. The common one’s are:

  • RichText
  • BlockControls
  • AlignmentToolbar
  • Inspect
  • ColorPalette

You can find components in the Gutenberg git repo
https://github.com/WordPress/gutenberg/blob/master/packages/block-editor/src/components/

1-RichText Component

Instead of creating DOM nodes using createElement(), we can encapsulate this behavior using Components . Components provide reusability and allow you to hide the complexity into their self-contained units. There are a number of components available. Let’s talk about one of them, called RichTextcomponent. It can be considered as textarea element that enables rich content editing including bold, italics, hyperlinks, etc.
You can find RichText Component defined in https://github.com/WordPress/gutenberg/blob/master/packages/block-editor/src/components/rich-text/index.js

Please make sure to add wp-editor to the dependency array of registered script handles when calling wp_register_script.

https://gist.github.com/imranhsayed/974a0dbd37ce30adc5e9de4298e3ec35