
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(); }); |
|
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) ]; }); |
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(); } } |
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> |
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'); } } |
Also, add a route to
routes/web.php file to pass down requests to index method.
|
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')); |
Now create
Photos component and add this code.
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'); |
|
public function getPhotos(){ return Photos::select('id', 'uri')->paginate(10); } |
In your
Photos component, add
getPhotos() method.
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)); } |
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'); } |
JSON paginated response looks like this.
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.
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.