Xây dựng một ứng dụng React với Laravel: Phần 2, React
Vietnamese (Tiếng Việt) translation by Thai An (you can also view the original English article)



Đây là phần hai và cũng là phần cuối của loạt bài viết hướng dẫn xây dựng ứng dụng React với back end sử dụng Laravel. Phần đầu của loạt bài này, chúng ta đã tạo một RESTful API bằng Laravel cho ứng dụng liệt kê danh sách sản phẩm. Trong bài hướng dẫn, chúng ta sẽ phát triển phần front end sử dụng React.
Chúng ta cũng sẽ cân nhắc tất cả những chọn lựa khả dĩ để bổ khuyết giữa Laravel và React. Bạn không phải theo dõi trước phần một để hiểu bài hướng dẫn này. Nếu bạn ở đây để xem xét React và Laravel hoà hợp cùng nhau, thực tế bạn có thể bỏ qua phần đầu tiên. Bạn nên đến thẳng Github, clone repo về, và xem nội dung trọng điểm bên dưới để bắt đầu.
Tóm gọn các trọng điểm
Trong bài trước, chúng ta đã phát triển ứng dụng Laravel để phản hồi với các yêu cầu API. Chúng ta đã tạo các route, một controller, một model cho ứng dụng đơn giản: liệt kê sản phẩm. Trong khi nhiệm vụ của controller là trả lời cho các yêu cầu HTTP, thì view hoàn toàn bị bỏ qua.
Sau đó, chúng ta thảo luận các kỹ thuật cho xử lý exception và xác thực bằng Laravel. Cuối bài hướng dẫn, chúng ta đã hoàn tất Laravel back-end API. Giờ ta có thể dùng API này để xây dựng các ứng dụng cho web và các thiết bị di động.
Trong bài hướng dẫn này, chúng ta sẽ tập trung sang phần front-end. Nửa phần đầu nói đến cách thiết lập React trong môi trường của Laravel. Tôi cũng sẽ giới thiệu cho bạn về Laravel Mix (được hỗ trợ kể từ bản Laravel 5.4), nó là một API cho các tài nguyên biên dịch. Ở nửa phần còn lại của bài viết, chúng ta sẽ bắt đầu xây dựng ứng dụng từ ban đầu.
Thiết lập React trong Laravel
Laravel Mix được giới thiệu trong Laravel 5.4, và hiện là phương pháp lý tưởng để gắn kết React và Laravel. Với Laravel 5.5, toàn bộ quá trình được thực hiện dễ dàng hơn. Tôi mô tả về hai phương pháp bên dưới.
Sử dụng React Preset Command (Laravel 5.5)
Laravel 5.5 có tính năng mới cho phép bạn scaffold mã lệnh cho component của React bằng câu lệnh artisan preset react. Trong các phiên bản trước của Laravel, việc thiết lập React bên trong Laravel không dễ dàng. Nếu bạn đang dùng bản Laravel mới nhất, hãy chạy lênh bên dưới để bổ sung thư viện React vào dự án của bạn.
1 |
php artisan preset react |
Mặc định thì Laravel có kèm theo thư viện Vue, và câu lệnh trên thay thế toàn bộ giá trị của Vue bằng React. Thật thú vị, nếu bạn không một thư viện, bạn có thể xoá chúng cùng lúc với câu lệnh php artisan preset none.
Nếu mọi thứ ổn, terminal sẽ hiển thị thế này.
1 |
React scaffolding installed successfully. |
2 |
Please run "npm install && npm run dev" to compile your fresh scaffolding.
|
Laravel Mix chạy ở background của Laravel, là một wrapper mượt mà cho webpack. Như bạn đã biết về Webpack, là một module bundler. Nó đảm trách tất cả phần module phụ thuộc, và tạo ra những tài nguyên tĩnh cho JavaScript và CSS. React cần có một module bundler, và webpack là chọn lựa hoàn hảo cho vị trí này. Vì vậy Laravel Mix là layer ở trên cùng cùa webpack và giúp Laravel làm việc với webpack dễ dàng hơn.
Hiểu rõ hơn cách Laravel Mix làm việc rất quan trọng nếu bạn cần thay đổi cấu hình của webpack sau này. Câu lệnh React preset không cho chúng ta biết cách mọi thứ vận hành ra sao ở dưới background. Vì vậy hãy xoá thư viện React và thay vào đó hãy truy nguyên lại các bước thao tác trước đó.
Phương pháp thủ công (Laravel 5.4)
Nếu bạn dùng Laravel 5.4, hoặc nếu bạn thắc mắc Laravel Mix sẽ được cấu hình thế nào, thì đây là các bước bạn cần làm theo:
Cài đặt react, react-dom, và babel-preset-react bằng npm. Sẽ tốt hơn nếu bạn cài đặt luôn yarn. Thực sự là Laravel và React thích yarn hơn npm.
Đến file webpack.mix.js nằm ở thư mục gốc của ứng dụng Laravel của bạn. Đây là file cấu hình để bạn khai báo cách biên dịch các thư viện của bạn. Thay thế dòng mix.js('resources/assets/js/app.js', 'public/js'); bằng mix.react('resources/assets/js/app.js', 'public/js');: app.js là điểm khởi chạy của các file JavaScript, và các file đã biên dịch sẽ nằm trong public/js. Chạy npm install trong terminal để cài đặt tất cả phần phụ thuộc.
Tiếp đó đi đến resources/assets/js. Đã có sẵn thư mục components và một vài file JavaScript. Các component React sẽ được đặt trong thư mục components. Xoá bỏ file Example.vue hiện có và tạo một file mới cho component React.
resources/assets/component/Main.js
1 |
import React, { Component } from 'react'; |
2 |
import ReactDOM from 'react-dom'; |
3 |
|
4 |
/* An example React component */
|
5 |
class Main extends Component { |
6 |
render() { |
7 |
return ( |
8 |
<div> |
9 |
<h3>All Products</h3> |
10 |
</div> |
11 |
);
|
12 |
}
|
13 |
}
|
14 |
|
15 |
export default Main; |
16 |
|
17 |
/* The if statement is required so as to Render the component on pages that have a div with an ID of "root";
|
18 |
*/
|
19 |
|
20 |
if (document.getElementById('root')) { |
21 |
ReactDOM.render(<Main />, document.getElementById('root')); |
22 |
}
|
Cập nhật app.js để xoá bỏ các code liên quan đến Vue và thay bằng cách component của React.
resources/assets/app.js
1 |
require('./bootstrap'); |
2 |
|
3 |
/* Import the Main component */
|
4 |
import Main from './components/Main'; |
Giờ chúng ta chỉ cần thiết lập cho View có thể truy xuất cho các tài nguyên. Các file về view nằm trong thư mục resources/views. Bổ sung thẻ <script> vào welcome.blade.php, đây là trang hiển thị mặc định khi bạn duyệt localhost:8000/. Remove nội dung của file này và thay vào code bên dưới.
resources/views/welcome.blade.php
1 |
<!doctype html>
|
2 |
<html lang="{{ app()->getLocale() }}"> |
3 |
<head>
|
4 |
<meta charset="utf-8"> |
5 |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> |
6 |
<meta name="viewport" content="width=device-width, initial-scale=1"> |
7 |
<title>Laravel React application</title> |
8 |
<link href="{{mix('css/app.css')}}" rel="stylesheet" type="text/css"> |
9 |
</head>
|
10 |
<body>
|
11 |
<h2 style="text-align: center"> Laravel and React application </h2> |
12 |
<div id="root"></div> |
13 |
<script src="{{mix('js/app.js')}}" ></script> |
14 |
</body>
|
15 |
</html>
|
Sau cùng, chạy lệnh npm run dev hoặc yarn run dev để biên dịch các tài nguyên. Nếu bạn duyệt localhost:8000, bạn sẽ thấy:



Package.json có một mã kịch bản theo dõi để tự động biên dịch các tài nguyên khi có thay đổi bất kỳ xảy ra. Để kích hoạt chế độ này, chọn npm run watch.
Chúc mừng bạn đã cấu hình thành công React để hoạt động cùng Laravel. Giờ hãy tạo một số component cho React ở front-end.
Phát triển ứng dụng React
Nếu bạn mới biết đến React, bạn sẽ thấy phần còn lại có phần thử thách. Tôi đề xuất bạn hãy xem loạt bài React Crash Course for Beginners để quen thuộc hơn với các khái niêm của React. Hãy cùng bắt đầu nào!
Ứng dụng React được xây dựng dựa trên component. Component là cấu trúc quan trọng nhất của React, và chúng ta có một thư mục chỉ dành cho component.
Component cho phép bạn phân nhỏ UI thành những phần độc lập và có thể sử dụng lại, và hãy tách biệt mỗi phần đó với nhau. Về khái niệm, component giống với các hàm của JavaScript. Chúng nhận các input tuỳ ý (gọi là "props") và trả về những thành phần React mô tả phần hiển thị trên màn hình.
-- Tài liệu chính thức của React
Trong ứng dụng ta đang xây dựng, chúng ta sẽ bắt đầu với component cơ bản cho phép hiển thị các sản phẩm được trả về từ máy chủ. Hãy gọi nó là Main component. Ban đầu, component sẽ đảm trách những việc dưới đây:
- Lấy tất cả sản phẩm từ API (GET /api/products).
- Lưu dữ liệu sản phẩm trong state.
- Hiển thị dữ liệu sản phẩm.
React không phải là một framework toàn diện, do đó bản thân thư viện này không có các tính năng Ajax. Tôi sẽ dùng fetch(), là một API JavaScript tiêu chuẩn để lấy dữ liệu từ máy chủ. Nhưng còn rất nhiều chọn lựa khác để gọi Ajax đến máy chủ.
resources/assets/js/component/Main.js
1 |
import React, { Component } from 'react'; |
2 |
import ReactDOM from 'react-dom'; |
3 |
|
4 |
/* Main Component */
|
5 |
class Main extends Component { |
6 |
|
7 |
constructor() { |
8 |
|
9 |
super(); |
10 |
//Initialize the state in the constructor
|
11 |
this.state = { |
12 |
products: [], |
13 |
}
|
14 |
}
|
15 |
/*componentDidMount() is a lifecycle method
|
16 |
* that gets called after the component is rendered
|
17 |
*/
|
18 |
componentDidMount() { |
19 |
/* fetch API in action */
|
20 |
fetch('/api/products') |
21 |
.then(response => { |
22 |
return response.json(); |
23 |
})
|
24 |
.then(products => { |
25 |
//Fetched product is stored in the state
|
26 |
this.setState({ products }); |
27 |
});
|
28 |
}
|
29 |
|
30 |
renderProducts() { |
31 |
return this.state.products.map(product => { |
32 |
return ( |
33 |
/* When using list you need to specify a key
|
34 |
* attribute that is unique for each list item
|
35 |
*/
|
36 |
<li key={product.id} > |
37 |
{ product.title } |
38 |
</li> |
39 |
);
|
40 |
})
|
41 |
}
|
42 |
|
43 |
render() { |
44 |
/* Some css code has been removed for brevity */
|
45 |
return ( |
46 |
<div> |
47 |
<ul> |
48 |
{ this.renderProducts() } |
49 |
</ul> |
50 |
</div> |
51 |
|
52 |
);
|
53 |
}
|
54 |
}
|
Chúng đã đang khởi tạo state của products vào một mảng rỗng trong constructor. Khi component đã hiển thị, chúng ta dùng fetch() để lấy sản phẩm từ /api/products và lưu nó vào state. Phương thức render (hiển thị) dùng để mô tả UI của component. Tất cả sản phẩm được hiển thị dưới dạng danh sách.



Trang này chỉ liệt kê các tên sản phẩm, khá tẻ nhạt. Hơn nữa, chúng ta không có các thành phần tương tác ở đó cả. Hãy làm tên sản phẩm có thể click vào được, và khi click vào sẽ có nhiều thông tin về sản phẩm đó.
Hiển thị thông tin sản phẩm
Đây là danh sách các thứ chúng ta cần thực hiện:
- Một state để theo dõi sản phẩm được click. Gọi tên nó là
currentProductvới giá trị ban đầu lànull. - Khi tên sản phẩm được click vào,
this.state.currentProductđược cập nhập. - Thông tin sản phẩm được quan tâm sẽ hiển thị bên phải. Trừ khi một sản phẩm được chọn, nếu không phần này sẽ hiển thị thông báo "No Product selected" (Chưa chọn sản phẩm).
resources/assets/js/component/Main.js
1 |
import React, { Component } from 'react'; |
2 |
import ReactDOM from 'react-dom'; |
3 |
|
4 |
/* Main Component */
|
5 |
class Main extends Component { |
6 |
|
7 |
constructor() { |
8 |
|
9 |
super(); |
10 |
|
11 |
/* currentProduct keeps track of the product currently
|
12 |
* displayed */
|
13 |
this.state = { |
14 |
products: [], |
15 |
currentProduct: null |
16 |
}
|
17 |
}
|
18 |
|
19 |
componentDidMount() { |
20 |
//code omitted for brevity
|
21 |
}
|
22 |
|
23 |
renderProducts() { |
24 |
return this.state.products.map(product => { |
25 |
return ( |
26 |
//this.handleClick() method is invoked onClick.
|
27 |
<li onClick={ |
28 |
() =>this.handleClick(product)} key={product.id} > |
29 |
{ product.title } |
30 |
</li> |
31 |
);
|
32 |
})
|
33 |
}
|
34 |
|
35 |
handleClick(product) { |
36 |
//handleClick is used to set the state
|
37 |
this.setState({currentProduct:product}); |
38 |
|
39 |
}
|
40 |
|
41 |
render() { |
42 |
/* Some css code has been removed for brevity */
|
43 |
return ( |
44 |
<div> |
45 |
<ul> |
46 |
{ this.renderProducts() } |
47 |
</ul> |
48 |
</div> |
49 |
|
50 |
);
|
51 |
}
|
52 |
}
|
Chúng ta bổ sung createProduct vào state và khởi tạo với giá trị null. Dòng onClick={ () =>this.handleClick(product) } gọi phương thức handleClick() khi một sản phẩm được click. Hàm handleClick() cập nhật state của currentProduct.
Giờ để hiển thị dữ liệu sản phẩm, chúng ta có thể render nó bên trong Main component hoặc tạo một component mới. Như đã đề cập, việc phân tách UI thành các component nhỏ hơn là cách vận hành của React. Vì thế chúng ta sẽ tạo một component mới và gọi là Product.
Product component này nằm bên trong Main component. Main component sẽ truyền state của nó như một props. Product component nhận props này làm input và hiển thị các thông tin liên quan.
resources/assets/js/component/Main.js
1 |
render() { |
2 |
return ( |
3 |
/* The extra divs are for the css styles */
|
4 |
<div> |
5 |
<div> |
6 |
<h3> All products </h3> |
7 |
<ul> |
8 |
{ this.renderProducts() } |
9 |
</ul> |
10 |
</div> |
11 |
|
12 |
<Product product={this.state.currentProduct} /> |
13 |
</div> |
14 |
);
|
15 |
}
|
16 |
}
|
resources/assets/js/component/Product.js
1 |
import React, { Component } from 'react'; |
2 |
|
3 |
/* Stateless component or pure component
|
4 |
* { product } syntax is the object destructing
|
5 |
*/
|
6 |
const Product = ({product}) => { |
7 |
|
8 |
const divStyle = { |
9 |
/*code omitted for brevity */
|
10 |
}
|
11 |
|
12 |
//if the props product is null, return Product doesn't exist
|
13 |
if(!product) { |
14 |
return(<div style={divStyle}> Product Doesnt exist </div>); |
15 |
}
|
16 |
|
17 |
//Else, display the product data
|
18 |
return( |
19 |
<div style={divStyle}> |
20 |
<h2> {product.title} </h2> |
21 |
<p> {product.description} </p> |
22 |
<h3> Status {product.availability ? 'Available' : 'Out of stock'} </h3> |
23 |
<h3> Price : {product.price} </h3> |
24 |
|
25 |
</div> |
26 |
)
|
27 |
}
|
28 |
|
29 |
export default Product ; |
Ứng dụng sẽ trông giống thế này:



Thêm một sản phẩm mới
Chúng ta đã triển khai thành công phần front-end tương ứng để lấy và hiển thị tất cả sản phẩm. Tiếp theo, chúng ta cần một form để thêm sản phẩm mới vào danh sách. Quá trình tạo sản phẩm mới có thể hơi phức tạp hơn việc lấy dự liệu từ API.
Đây là điều kiện cần để phát triển tính năng này:
- Một component dạng stateful (có trạng thái) dùng để hiển thị UI cho một form nhập liệu. State của component lưu giữ dữ liệu của form.
- Khi submit, component thứ cấp truyền state này đến Main component bằng callback.
- Main component có một phương thức, gọi là
handleNewProduct(), xử lý phần logic để bắt đầu một yêu cầu POST. Dựa trên phản hồi nhận được, Main component cập nhật state của nó (cảthis.state.productsvàthis.state.currentProduct).
Điều này không quá phức tạp phải không? Hãy thực hiện từng bước nào. Đầu tiên, tạo một component mới. Tôi sẽ gọi tên là AddProduct.
resources/assets/js/component/AddProduct.js
1 |
class AddProduct extends Component { |
2 |
|
3 |
constructor(props) { |
4 |
super(props); |
5 |
/* Initialize the state. */
|
6 |
this.state = { |
7 |
newProduct: { |
8 |
title: '', |
9 |
description: '', |
10 |
price: 0, |
11 |
availability: 0 |
12 |
}
|
13 |
}
|
14 |
|
15 |
//Boilerplate code for binding methods with `this`
|
16 |
this.handleSubmit = this.handleSubmit.bind(this); |
17 |
this.handleInput = this.handleInput.bind(this); |
18 |
}
|
19 |
|
20 |
/* This method dynamically accepts inputs and stores it in the state */
|
21 |
handleInput(key, e) { |
22 |
|
23 |
/*Duplicating and updating the state */
|
24 |
var state = Object.assign({}, this.state.newProduct); |
25 |
state[key] = e.target.value; |
26 |
this.setState({newProduct: state }); |
27 |
}
|
28 |
/* This method is invoked when submit button is pressed */
|
29 |
handleSubmit(e) { |
30 |
//preventDefault prevents page reload
|
31 |
e.preventDefault(); |
32 |
/*A call back to the onAdd props. The current
|
33 |
*state is passed as a param
|
34 |
*/
|
35 |
this.props.onAdd(this.state.newProduct); |
36 |
}
|
37 |
|
38 |
render() { |
39 |
const divStyle = { |
40 |
/*Code omitted for brevity */ } |
41 |
|
42 |
return( |
43 |
<div> |
44 |
<h2> Add new product </h2> |
45 |
<div style={divStyle}> |
46 |
/*when Submit button is pressed, the control is passed to
|
47 |
*handleSubmit method
|
48 |
*/
|
49 |
<form onSubmit={this.handleSubmit}> |
50 |
<label> Title: |
51 |
{ /*On every keystroke, the handeInput method is invoked */ } |
52 |
<input type="text" onChange={(e)=>this.handleInput('title',e)} /> |
53 |
</label> |
54 |
|
55 |
<label> Description: |
56 |
<input type="text" onChange={(e)=>this.handleInput('description',e)} /> |
57 |
</label> |
58 |
|
59 |
{ /* Input fields for Price and availability omitted for brevity */} |
60 |
|
61 |
<input type="submit" value="Submit" /> |
62 |
</form> |
63 |
</div> |
64 |
</div>) |
65 |
}
|
66 |
}
|
67 |
|
68 |
export default AddProduct; |
69 |
Component này cơ bản hiển thị một form nhập liệu, và tất cả giá trị nhập được lưu trong state (this.state.newProduct). Sau đó, khi nộp form, phương thức handleSubmit() được gọi. Nhưng AddProduct cần giao tiếp thông tin lại với cấp cao của nó, và chúng ta làm điều này qua một callback.
Main component, là cấp cao, sẽ truyền một tham chiếu hàm như props. Component con, AddProduct gọi props này để thông báo với cấp cao về trạng thái đã thay đổi. Cho nên dòng this.props.onAdd(this.state.newProduct); là một ví dụ của hàm callback dùng để thông báo cho component cấp cao của sản phẩm mới.
Giờ thì bên trong Main component, chúng ta sẽ khai báo <AddProduct /> như sau:
1 |
<AddProduct onAdd={this.handleAddProduct} /> |
Trình xử lý sự kiện onAdd được nối tiếp với phương thức handleAddProduct() của component. Phương thức này chứa mã lệnh để gọi yêu cầu POST đến máy chủ. Nếu phản hồi báo tin rằng sản phẩm được tạo thành công, thì state của products và currentProducts được cập nhật.
1 |
handleAddProduct(product) { |
2 |
|
3 |
product.price = Number(product.price); |
4 |
/*Fetch API for post request */
|
5 |
fetch( 'api/products/', { |
6 |
method:'post', |
7 |
/* headers are important*/
|
8 |
headers: { |
9 |
'Accept': 'application/json', |
10 |
'Content-Type': 'application/json' |
11 |
},
|
12 |
|
13 |
body: JSON.stringify(product) |
14 |
})
|
15 |
.then(response => { |
16 |
return response.json(); |
17 |
})
|
18 |
.then( data => { |
19 |
//update the state of products and currentProduct
|
20 |
this.setState((prevState)=> ({ |
21 |
products: prevState.products.concat(data), |
22 |
currentProduct : data |
23 |
}))
|
24 |
})
|
25 |
|
26 |
}
|
Đừng quên gán phương thức handleProduct vào class qua this.handleAddProduct = this.handleAddProduct.bind(this) trong constructor. Và đây là bản hoàn tất của ứng dụng.



Tiếp theo là gì?
Ứng dụng chưa hoàn tất khi không có tính năng xoá và cập nhật. Nhưng nếu bạn đã thực hành sát theo bài hướng dẫn đến tận lúc này, bạn có thể hoàn tất phần còn thiếu mà không gặp rắc rối nào. Để bạn bắt đầu, tôi đã cung cấp cho bạn logic của trình xử lý sự kiện cho cả tình huống xoá và cập nhật.
Logic để xoá một sản phẩm
1 |
handleDelete() { |
2 |
|
3 |
const currentProduct = this.state.currentProduct; |
4 |
fetch( 'api/products/' + this.state.currentProduct.id, |
5 |
{ method: 'delete' }) |
6 |
.then(response => { |
7 |
/* Duplicate the array and filter out the item to be deleted */
|
8 |
var array = this.state.products.filter(function(item) { |
9 |
return item !== currentProduct |
10 |
});
|
11 |
|
12 |
this.setState({ products: array, currentProduct: null}); |
13 |
|
14 |
});
|
15 |
}
|
Logic để cập nhật một sản phẩm
1 |
handleUpdate(product) { |
2 |
|
3 |
const currentProduct = this.state.currentProduct; |
4 |
fetch( 'api/products/' + currentProduct.id, { |
5 |
method:'put', |
6 |
headers: { |
7 |
'Accept': 'application/json', |
8 |
'Content-Type': 'application/json' |
9 |
},
|
10 |
body: JSON.stringify(product) |
11 |
})
|
12 |
.then(response => { |
13 |
return response.json(); |
14 |
})
|
15 |
.then( data => { |
16 |
/* Updating the state */
|
17 |
var array = this.state.products.filter(function(item) { |
18 |
return item !== currentProduct |
19 |
})
|
20 |
this.setState((prevState)=> ({ |
21 |
products: array.concat(product), |
22 |
currentProduct : product |
23 |
}))
|
24 |
})
|
25 |
}
|
Điều bạn cần tiếp theo là đào sâu, tự mình thực hành, và hoàn tất ứng dụng với logic phía trên. Tôi sẽ gợi ý cho bạn: nút xoá lý tưởng nên nằm trong component Product, trong khi đó tính năng cập nhật nên có một component riêng cho nó. Tôi khuyến khích bạn thực hiện thử thách này và hoàn tất các component còn thiếu.
Tóm tắt
Chúng ta đã trải qua một quá trình từ đầu đến giờ. Đầu tiên, ta tạo REST API bằng framework Laravel. Sau đó, chúng ta thảo luận các chọn lựa cho Laravel và React. Cuối cùng, chúng ta xây dựng một front-end cho Laravel bằng React.
Dù chúng ta chủ yếu tập trung vào việc tạo ra ứng dụng một trang bằng React, nhưng bạn có thể tạo các widget hoặc component để có thể đưa vào các thành phần cụ thể trong view của bạn. React rất linh hoạt vì nó là một thư viện tốt.
Trải qua vài năm, React đã trở nên phổ biến hơn. Sự thật thì chúng tôi đã có một số lượng sản phẩm trên thị trường để bán, xét duyệt, triển khai, và nhiều nữa. Nếu bạn đang tìm kiếm các tài nguyên bổ sung cho React, đừng do dự, hãy xem qua chúng.
Bạn từng trải nghiệm Laravel với React chưa? Bạn nghĩ gì? Hãy chia sẻ qua phần bình luận.



