Laravel has built-in easy-to-use pagination of database results out of the box. Today I will show you how to use Laravel built in Pagination along with React Paginate which is a small easy to use ReactJS component to render pagination to build a custom simple pagination as quick as possible by Injecting React component in your existing, blade template. I will be also using React Progress 2 to give a YouTube-style progress bar and Axios for making HTTP API request to laravel backend and Moment.js for easy formatting the timestamp. If you are completely new to React or ES6 terminologies then I really suggest you learn the basics before continuing this tutorial as React is a little more complicated and confusing than jQuery or Vue.js although I try to make it as simple as possible.
First thing is to include React in our application. In the case of Laravel, you can easily include React using Laravel Mix.
Laravel Mix provides a fluent API for defining Webpack build steps for your Laravel application using several common CSS and JavaScript pre-processors. All you need to do is install Nodejs and NPM in your development system and run
npm install
to install all javascript dependencies in your application. By default jQuery, Bootstrap, Vuejs, Axios and Lodash are already included in this setup. Now just run
npm run watch
to compile your assets. Don't forget to include app.css and app.js files in the public folders to your Laravel View Layouts.
Now there is a little twist. If you are not using Vue.js in your laravel projects then you can replace the Vue.js with React by just running the following commands.
php artisan preset react
This command will swap out the Vue scaffolding with React scaffolding. The default Mix configuration, components, and all other related files will be updated accordingly. Don't forget to run npm install
and npm run watch
after executing this command. Please note that this will work only in Laravel 5.5 and above.
Now if you want to include React to an existing Laravel project (5.4 and above) without removing Vue.js then you can follow below instructions to include React without disrupting your old Vue components.
Replace your webpack.mix.js file with following snippets.
let mix = require('laravel-mix');
/*
|--------------------------------------------------------------------------
| Mix Asset Management
|--------------------------------------------------------------------------
|
| Mix provides a clean, fluent API for defining some Webpack build steps
| for your Laravel application. By default, we are compiling the Sass
| file for the application as well as bundling up all the JS files.
|
*/
mix.js('resources/assets/js/app.js', 'public/js')
.react('resources/assets/js/react.js', 'public/js')
.sass('resources/assets/sass/app.scss', 'public/css');
As you can see that I am bundling my React components into a separate output file ie to public/js/react.js
file. Don't forget to include this file in your blades where you use React components.
After modifying webpack.mix.js file install the latest version or React and ReactDOM as your development dependency by running the following command.
npm install react react-dom --save-dev
Then run,
npm run watch
This command will install babel-preset-react
for the first time and now everything is done and you successfully included React in your Laravel project.
Add the following route to your web.php file
Route::get('demos/react-pagination','DemoController@viewReactPagination');
Route::post('demos/react-pagination','DemoController@getReactPagination');
The first route is for showing the Initial View Page and the second route is used by ajax call for fetching JSON search result from the database.
Now Let's create our DemoController by running the following command.
php artisan make:controller DemoController
The complete code for DemoController is given below.
<?php
namespace App\Http\Controllers;
use Helper;
use App\Post;
use Illuminate\Http\Request;
class DemoController extends Controller
{
/**
* To display the show page
*
* @return \Illuminate\Http\Response
*/
public function viewReactPagination()
{
return view('demos.reactPagination');
}
/**
* To get post details for react pagination
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function getReactPagination(Request $request)
{
$posts = Post::where('published',1)
->with('tags')
->orderBy('created_at', 'DESC')
->paginate(5);
foreach ($posts as $key => $post) {
$post->image = Helper::catch_first_image($post->body);
$post->url = url('blog/'.$post->slug);
$post->body = strip_tags(str_limit($post->body,350));
}
return [
'status' => "success",
'data' => [
'posts' => $posts
]
];
}
}
The viewReactPagination method is used to just show our view page and getReactPagination does the actual database querying and fetching of the result. Although you can directly return $posts after the database querying I am not doing that because in order to fetch post images I need to call an external Helper function. So I am using the foreach method in Laravel Collection to run over the original collection and then to add an additional field - image to each post. I am also stripping down the post body and create slug URLs inside the foreach loop. Remember the foreach loop is a mutable method ie it overrides the original collection to add or modify attributes. Since it is a mutable method use it with caution to avoid unwanted modification. In this scenario, it is completely ok as we are immediately returning the result back as JSON. If you are wondering about catch_first_image custom helper function and how to create it you can refer the following Link.
Note: Both controller and routes parts will exclusively depend on your projects you are working on. They are unnecessary in this tutorial as only this they do is give some response data. I just added them only for reference purpose.
Now let's create a demo/reactPagination.blade.php file to include our where we inject our main React Component. The complete code of this file is given below.
@extends('main')
@section('title','Pagination using Laravel and React Demo -')
@section('content')
<div class="uk-container uk-margin">
<div class="uk-grid uk-margin-top uk-flex-center" data-ukgrid>
<div class="uk-width-1-1">
<h3 class="uk-heading-line uk-text-center uk-text-bold uk-margin m-b-50">
<span>Pagination using Laravel and React - Demo</span>
</h3>
</div>
<!-- Inject React Infinite Scroll Component -->
<div class="uk-width-3-4" id="react-pagination"></div>
</div>
</div>
@stop
Don't forget to include your bundled js file at bottom of your view file. Remember you will see some fancy class names but don't worry about it as CSS and HTML parts changes according to your requirements. I used UIKIt 3 to create ShareurCodes.
Before you create your react components, don't forget to install Moment.js and include your main class based Parent React Component in your resourses/assets/js/app.js or in the resourses/assets/js/react.js file (If you are including React alongside with Vue and want to bundle it separately) as below.
You can install Moment.js by using the following command
npm install moment --save-dev
After installing Moment.js, Include Moment.js and Main Class based Parent React Component for the Infinite scroll in your app.js file as follows.
window.moment = require('moment');
// React Components
require('./components/ReactPagination/ReactPagination');
The other dependencies we needed are react-paginate and React Progress 2. You can install react-paginate and react-progress-2 by using the following command
npm install react-paginate react-progress-2 --save-dev
Now Let's create our primary React Component. The complete code for the components/ReactPagination/ReactPagination file is given below.
import React from 'react';
import ReactDOM from 'react-dom';
import ReactPaginate from 'react-paginate';
import Progress from 'react-progress-2';
import 'react-progress-2/main.css';
import PostContent from './PostContent';
class ReactPagination extends React.Component {
constructor(props) {
super(props);
this.state = {
posts: [],
completed: false,
pageCount: 1,
currentPage: 1
};
this.handlePageClick = this.handlePageClick.bind(this);
}
getQueryStringValue(key) {
const value = decodeURIComponent(
window.location.search.replace(
new RegExp(
'^(?:.*[&\\?]' +
encodeURIComponent(key).replace(/[\.\+\*]/g, '\\$&') +
'(?:\\=([^&]*))?)?.*$',
'i'
),
'$1'
)
);
return value ? value : null;
}
async componentDidMount() {
const page = this.getQueryStringValue('page');
await Promise.resolve(
this.setState(() => ({ currentPage: page ? page : 1 }))
);
this.getPostData();
}
async handlePageClick(data) {
const page = data.selected >= 0 ? data.selected + 1 : 0;
await Promise.resolve(this.setState(() => ({ currentPage: page })));
this.getPostData();
}
async getPostData() {
Progress.show();
if (history.pushState) {
const newUrl =
window.location.protocol +
'//' +
window.location.host +
window.location.pathname +
'?page=' +
this.state.currentPage;
window.history.pushState({ path: newUrl }, '', newUrl);
const response = await axios.post(newUrl);
try {
if (response.data.status == 'success') {
this.setState(() => ({
posts: response.data.data.posts.data,
currentPage: response.data.data.posts.current_page,
pageCount: response.data.data.posts.last_page
}));
window.scrollTo(0, 0);
Progress.hide();
} else {
Progress.hide();
console.log(error);
}
} catch (error) {
Progress.hide();
console.log(error);
}
}
}
render() {
const Posts = this.state.posts.map(post => (
<PostContent key={post.id} post={post} />
));
return (
<div>
<Progress.Component
style={{ background: '#99999978', height: '5px' }}
thumbStyle={{ background: '#5900b3', height: '5px' }}
/>
{Posts.length > 0 && Posts}
<ReactPaginate
pageCount={this.state.pageCount}
initialPage={this.state.currentPage - 1}
forcePage={this.state.currentPage - 1}
pageRangeDisplayed={4}
marginPagesDisplayed={2}
previousLabel="❮"
nextLabel="❯"
containerClassName="uk-pagination uk-flex-center"
activeClassName="uk-active"
disabledClassName="uk-disabled"
onPageChange={this.handlePageClick}
disableInitialCallback={true}
/>
</div>
);
}
}
if (document.getElementById('react-pagination')) {
ReactDOM.render(
<ReactPagination />,
document.getElementById('react-pagination')
);
}
We fetch JSON data from the server by calling the getPostData method using axios on componentDidMount lifecycle method of React. and also when the user clicks on Pagination links created by react paginate. I am using PostContent a functional component for rendering the post contents which is called by map function in javascript. The getQueryStringValue method is used to grab the initial page number from the URL of the page and assign it to the initial state of currentPage.
Remember in this demo I am using async / await instead of using pure Promise syntax to make syntax easy to read. If you wonder why we use promises in componentDidMount and handlePageClick methods, the reason is the when I call this.setState method by passing an anonymous function as the argument then it will become an asynchronous time taking method. So if we call getPostData method without waiting for state change completion unwanted pagination results will happen.
The Progress.show() and Progress.hide() are to show and hide progress bar at top of the page. It is a completely optional component. You can use other progress bars or library if you want. The React paginate gives selected page number as an argument to the call back function. Since it is starting from zero and laravel pagination starts from One I increase it by one each time I set the currentPage state. The window.scrollTo(0, 0) is used to scroll the page to top each time new post rendered.
The complete code for the components/ReactPagination/PostContent.js file is given below.
import React from 'react';
const PostContent = (props) => (
<div className="uk-card uk-card-default uk-card-small uk-card-body uk-border-rounded custom-border uk-margin" >
<div className="uk-grid uk-grid-medium uk-flex uk-flex-middle" data-uk-grid>
<div className="uk-width-1-1">
<h4 className="uk-card-title uk-margin-remove-top uk-margin-remove-bottom">
<a className="uk-link-reset uk-text-bold" href={props.post.url}>
{props.post.title}
</a>
</h4>
<span className="uk-article-meta">
<span data-uk-icon="calendar"></span>
Published at {moment(props.post.created_at).format('lll')}
</span>
</div>
<div className="uk-width-1-3@s uk-width-2-6@m uk-height-1-1 uk-margin-small-top">
<img data-src={props.post.image} alt={props.post.title } data-uk-img />
</div>
<div className="uk-width-2-3@s uk-width-4-6@m uk-margin-small-top">
<p className="uk-margin-small uk-text-justify">
{props.post.body}
</p>
<a data-uk-icon="icon: arrow-right" href={props.post.url} title="Read More"
className="uk-button uk-button-link uk-button-small uk-text-bold">Read More</a>
</div>
</div>
</div>
);
export default PostContent;
I am using moment for formatting created at the field from laravel to a more human readable one. Since I am using UIkit 3 instead of Bootstrap you will see so many data attributes like data-src. Don't confuse, it has no relation to React.
Note: If you are using these components outside Laravel projects don't forget to globally include Axios.
The output image of the above demo application is given below.
You can demo the above application by visiting the following link.
https://shareurcodes.com/demos/react-infinite-scroll
If anybody has any suggestions or doubts or need any help comment below and I try will respond to every one of you as early as possible.