Laravel Paginator makes it a breeze to paginate database results and render them in blade views. The HTML generated by paginator is compatible with bootstrap but you can customize it according to your needs. Paginator implements Illuminate\Contracts\Support\Jsonable interface to expose a toJson method to convert paginated results to JSON. In this tutorial, I’ll show you how to implement infinite scrolling in React application using Laravel’s paginator.

Getting Started

After setting up a new project, run php artisan preset react && npm install && npm run watch to generate React scaffolding and install dependencies.

Now create a Photos model along with its migration to create a database table.

php artisan make:model Photos --migration

Schema::create('photos', function (Blueprint $table) { $table->increments('id'); $table->string('uri'); $table->timestamps(); }); 1 2 3 4 5 Schema:: create ( 'photos' , function ( Blueprint $table ) { $table -> increments ( 'id' ) ; $table -> string ( 'uri' ) ; $table -> timestamps ( ) ; } ) ;

class Photos extends Model { protected $table = 'photos'; } 1 2 3 4 class Photos extends Model { protected $table = 'photos' ; }

Populate Database

Create a factory to insert fake images in photos table.

php artisan make:factory PhotosFactory --model=Photos

$factory->define(App\Photos::class, function (Faker $faker) { return [ 'uri' => $faker->imageUrl(640, 480, 'cats', true) ]; }); 1 2 3 4 5 $factory -> define ( App \ Photos:: class , function ( Faker $faker ) { return [ 'uri' = > $faker -> imageUrl ( 640 , 480 , 'cats' , true ) ] ; } ) ;

We’re using Faker to populate photos table with images of cats. Also, create a seeder to populate photos table with PhotosFactory.

php artisan make:seeder PhotosSeeder

class PhotosSeeder extends Seeder { public function run() { factory(App\Photos::class, 100)->create(); } } 1 2 3 4 5 6 7 class PhotosSeeder extends Seeder { public function run ( ) { factory ( App \ Photos:: class , 100 ) -> create ( ) ; } }

Run php artisan db:seed --class=PhotosSeeder to populate photos table with 100 records.

Bootstrap React SPA

Create index view in views folder to bootstrap Javascript and CSS.

<!doctype html> <html lang="en"> <head> <title>Laravel Infinite Scroll</title> <link rel="stylesheet" href="{{mix('/css/app.css')}}"> </head> <body> <div id="root"></div> <script src="{{mix('/js/app.js')}}"></script> </body> </html> 1 2 3 4 5 6 7 8 9 10 11 <!doctype html> <html lang = "en" > <head> <title> Laravel Infinite Scroll </title> <link rel = "stylesheet" href = "{{mix('/css/app.css')}}" > </head> <body> <div id = "root" > </div> <script src = "{{mix('/js/app.js')}}" > </script> </body> </html>

Create a Controller and add index() method to render this index view.

php artisan make:controller AppController

class AppController extends Controller { public function index(){ return view('index'); } } 1 2 3 4 5 6 class AppController extends Controller { public function index ( ) { return view ( 'index' ) ; } }

Also, add a route to routes/web.php file to pass down requests to index method.

Route::get('/', 'AppController@index'); 1 Route:: get ( '/' , 'AppController@index' ) ;

Frontend

Create App component in resources/assets/js/app.js and render it to #root DOM.

export default class App extends Component { render() { return ( <div className="container"> <Photos/> </div> ); } } ReactDOM.render(<App />, document.getElementById('root')); 1 2 3 4 5 6 7 8 9 10 11 export default class App extends Component { render ( ) { return ( < div className = "container" > < Photos / > < / div > ) ; } } ReactDOM . render ( < App / > , document . getElementById ( 'root' ) ) ;

Now create Photos component and add this code.

export default class Photos extends Component { constructor(props){ super(props); this.state = { photos : [], next_page : '/photos', loading : false } } render() { return ( <div className="photos mr-auto ml-auto col-xs-12 col-sm-12 col-md-8 col-lg-8"> </div> ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 export default class Photos extends Component { constructor ( props ) { super ( props ) ; this . state = { photos : [ ] , next_page : '/photos' , loading : false } } render ( ) { return ( < div className = "photos mr-auto ml-auto col-xs-12 col-sm-12 col-md-8 col-lg-8" > < / div > ) ; } }

We’ll store loaded images in photos[] state. next_page state is initially set to /photos route. We’ll use it keep track of paginated URLs and make XHR calls to the backend to retrieve paginated JSON results. We’ll mark loading state as true before making XHR calls to avoid multiple requests.

Add /photos route to routes file and add getPhotos() method to your controller to return paginated results for photos table.

Route::get('/photos', 'AppController@getPhotos'); 1 Route:: get ( '/photos' , 'AppController@getPhotos' ) ;

public function getPhotos(){ return Photos::select('id', 'uri')->paginate(10); } 1 2 3 public function getPhotos ( ) { return Photos:: select ( 'id' , 'uri' ) -> paginate ( 10 ) ; }

In your Photos component, add getPhotos() method.

componentDidMount(){ this.getPhotos(); } getPhotos(){ if(!this.state.loading){ // Set loading state to true to // avoid multiple requests on scroll this.setState({ loading : true, }); // register scroll event this.registerScrollEvent(); // make XHR request axios.get(this.state.next_page) .then((response) => { const paginator = response.data, photos = paginator.data; if(photos.length){ // add new this.setState({ photos : [...this.state.photos , ...photos], next_page : paginator.next_page_url, loading: false, }); } // remove scroll event if next_page_url is null if(!paginator.next_page_url){ this.removeScrollEvent(); } }); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 componentDidMount ( ) { this . getPhotos ( ) ; } getPhotos ( ) { if ( ! this . state . loading ) { // Set loading state to true to // avoid multiple requests on scroll this . setState ( { loading : true , } ) ; // register scroll event this . registerScrollEvent ( ) ; // make XHR request axios . get ( this . state . next_page ) . then ( ( response ) = > { const paginator = response . data , photos = paginator . data ; if ( photos . length ) { // add new this . setState ( { photos : [ . . . this . state . photos , . . . photos ] , next_page : paginator . next_page_url , loading : false , } ) ; } // remove scroll event if next_page_url is null if ( ! paginator . next_page_url ) { this . removeScrollEvent ( ) ; } } ) ; } }

In getPhotos() method, we’re updating loading state to true to avoid multiple XHR requests in case user aggressively scrolls down. After that, we’re calling registerScrollEvent() method. Let’s define it in our component.

registerScrollEvent(){ $(window).on('scroll', function() { if($(window).scrollTop() + $(window).height() === $(document).height()) { this.getPhotos(); } }.bind(this)); } 1 2 3 4 5 6 7 8 registerScrollEvent ( ) { $ ( window ) . on ( 'scroll' , function ( ) { if ( $ ( window ) . scrollTop ( ) + $ ( window ) . height ( ) === $ ( document ) . height ( ) ) { this . getPhotos ( ) ; } } . bind ( this ) ) ; }

We’re registering a scroll event. We’ll call getPhotos() method again if a user has scrolled down to the bottom of the page. We’ll remove this event handler if there are no more posts to load after XHR call.

removeScrollEvent(){ $(window).off('scroll'); } 1 2 3 removeScrollEvent ( ) { $ ( window ) . off ( 'scroll' ) ; }

JSON paginated response looks like this.

{ "current_page": 1, "data": [ { "id": 1, "uri": "https://lorempixel.com/640/480/cats/?88370" } ], "first_page_url": "http://127.0.0.1:3000/photos?page=1", "from": 1, "last_page": 10, "last_page_url": "http://127.0.0.1:3000/photos?page=10", "next_page_url": "http://127.0.0.1:3000/photos?page=2", "path": "http://127.0.0.1:3000/photos", "per_page": 10, "prev_page_url": null, "to": 10, "total": 100 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { "current_page" : 1 , "data" : [ { "id" : 1 , "uri" : "https://lorempixel.com/640/480/cats/?88370" } ] , "first_page_url" : "http://127.0.0.1:3000/photos?page=1" , "from" : 1 , "last_page" : 10 , "last_page_url" : "http://127.0.0.1:3000/photos?page=10" , "next_page_url" : "http://127.0.0.1:3000/photos?page=2" , "path" : "http://127.0.0.1:3000/photos" , "per_page" : 10 , "prev_page_url" : null , "to" : 10 , "total" : 100 }

We’ll update next_page state on our component with next_page_url response. Paginator will return a null value for next_page_url if there’s are no more records to load.

Update Photos component render method to display images.

render() { return ( <div className="photos mr-auto ml-auto col-xs-12 col-sm-12 col-md-8 col-lg-8"> {this.state.photos.length && this.state.photos.map((post) =>{ return ( <div key={post.id} className="photo mb-3"> <h3> Image ID # {post.id} </h3> <img src={post.uri} alt=""/> </div> ) }) } <div className="loading-spinner"> <ScaleLoader color={'#292929'} loading={this.state.loading} /> </div> </div> ); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 render ( ) { return ( < div className = "photos mr-auto ml-auto col-xs-12 col-sm-12 col-md-8 col-lg-8" > { this . state . photos . length && this . state . photos . map ( ( post ) = > { return ( < div key = { post . id } className = "photo mb-3" > < h3 > Image ID # { post . id } < / h3 > < img src = { post . uri } alt = "" / > < / div > ) } ) } < div className = "loading-spinner" > < ScaleLoader color = { '#292929' } loading = { this . state . loading } / > < / div > < / div > ) ; }

We’ll display ScaleLoader component from react-spinners package on loading. We’ll pass down the loading state of our Photos component and it will appear or disappear based on its current boolean value.

I’ve created a GitHub repository for example code. If you run into any issues, please leave a comment.