Questa è la seconda e ultima parte della serie sulla creazione di un’applicazione React con un back-end Laravel. Nella prima parte, abbiamo creato un’API RESTful utilizzando Laravel per un’applicazione base di catalogo prodotti. In questo tutorial, svilupperemo il front-end utilizzando React.
Non è necessario aver seguito la prima parte della serie per comprendere questo tutorial. Se sei qui per vedere come se la cavano React e Laravel insieme, puoi, infatti, evitare la prima parte. Dovresti andare su GitHub, clonare la repository e seguire le indicazioni di questo articolo.
Un rapido riepilogo
Nel tutorial precedente, abbiamo sviluppato un’applicazione Laravel che risponde alle chiamate API. Abbiamo creato Routes, un Controller e un Model per l’applicazione di catalogo prodotti. Poiché era compito del Controller restituire una risposta alle richieste HTTP, la sezione di visualizzazione è stata interamente ignorata.
Poi abbiamo spiegato le tecniche per la gestione e la convalida delle eccezioni utilizzando Laravel. Alla fine del tutorial, avevamo un’API back-end di Laravel. Ora possiamo utilizzare questa API per creare applicazioni sia per il web che per un’ampia gamma di dispositivi mobili.
In questo tutorial, sposteremo la nostra attenzione verso il front-end. La prima metà del tutorial riguarda la configurazione di React in un ambiente Laravel. Nella seconda metà del tutorial, inizieremo a creare un’applicazione React da zero.
Configurazione di React in Laravel
Laravel Mix, che nelle versioni precedenti veniva usato per configurare React, è stato sostituito da Vite nella versione 10 di Laravel.
Vite è un moderno strumento di creazione front-end che fornisce un ambiente di sviluppo estremamente veloce e raggruppa il tuo codice per la messa in produzione. Quando crei applicazioni con Laravel, in genere utilizzerai Vite per raggruppare i file CSS e JavaScript della tua applicazione in risorse pronte per l’ambiente di produzione. Laravel si integra perfettamente con Vite fornendo un plug-in ufficiale e una direttiva Blade per caricare le tue risorse in sviluppo e in produzione.
Per procedere, devi assicurarti che Node.js (16+) e NPM siano installati prima di eseguire Vite e Laravel Vite plugin:
1
2
|
node -v
npm -v
|
All’interno di Laravel, troverai un file package.json nella root della tua applicazione. Il file package.json predefinito include già tutto il necessario per iniziare a utilizzare Vite e il plug-in Laravel. Puoi installare le dipendenze front-end della tua applicazione tramite NPM:
1 |
npm install
|
Se vuoi creare il tuo front-end utilizzando React, dovrai installare anche il plug-in @vitejs/plugin-react
:
1 |
npm install --save-dev @vitejs/plugin-react
|
Puoi quindi includere il plug-in nel tuo file di configurazione vite.config.js
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react(),
laravel({
input: [
'resources/js/App.jsx',
],
refresh: true,
}),
],
});
|
Dovrai assicurarti che tutti i file contenenti JSX abbiano un’estensione .jsx
o .tsx
Dovrai anche includere la direttiva @viteReactRefresh
Blade aggiuntiva accanto alla direttiva @vite
esistente, nel file resources/views/welcome.blade.php.
1
2
3
4
5
6
|
<body>
<div class="container" id="App">
</div>
@viteReactRefresh
@vite(['resources/js/App.jsx'])
</body>
|
La direttiva @viteReactRefresh
deve essere chiamata prima della direttiva @vite
.
Infine, esegui npm run dev
o yarn run dev
per compilare le risorse.
La porta di default di Vite è 5173, mentre per Laravel controlla che sul file .env sia presente APP_URL=http://localhost:8000
. Poi lancia il comando php artisan serve
da un altra shell e vai su localhost:8000, da qui in poi tutte le modifiche su React verranno visualizzate sul suddetto indirizzo.
Sviluppo dell’applicazione React
Un’applicazione React è costruita attorno ai componenti. I componenti sono la struttura più importante in React e quindi avranno una directory dedicata.
I componenti ti consentono di suddividere l’interfaccia utente in parti indipendenti e riutilizzabili, e di pensare a ciascuna parte isolatamente. Concettualmente, i componenti sono come le funzioni JavaScript. Accettano input arbitrari (chiamati “props”) e restituiscono elementi React che descrivono cosa dovrebbe apparire sullo schermo.
Per l’applicazione che stiamo costruendo, inizieremo con un componente base che visualizza tutti i prodotti restituiti dal server. Il componente dovrebbe occuparsi inizialmente delle seguenti cose:
- Recupera tutti i prodotti dall’API (GET /api/products).
- Memorizza i dati del prodotto nel suo stato.
- Visualizza i dati del prodotto.
React non è un framework completo, non ha alcuna funzionalità AJAX inclusa. Quindi useremo fetch()
, un’API JavaScript standard per recuperare i dati dal server, ma ci sono tantissime alternative per effettuare chiamate AJAX al server, come Axios.
Useremo due hook: useState
e useEffect
(introdotte in React 16.8), metodi moderni per aggiornare lo stato di un componente e avviare le azioni del ciclo di vita rispettivamente in React. Per questo progetto stiamo utilizzando le ultime versioni di react
e react-dom
1
2
|
"react": "^18.2.0",
"react-dom": "^18.2.0",
|
Esegui eventualmente npm update
per aggiornare entrambe le librerie alle versioni più recenti. Ora che abbiamo tutte le ultime funzionalità di React, modificheremo il nostro primo componente.
Vi anticipo che la struttura finale dei componenti sarà la seguente:
Visualizzazione dei prodotti
resources/assets/js/component/Home.jsx
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
|
import React, { useState, useEffect } from 'react';
import Product from './Product';
import AddProduct from './AddProduct';
import '../../css/app.css';
const Home = () => {
const [products, setProducts] = useState([]);
const [currentProduct, setCurrentProduct] = useState(null);
const getProducts = () => {
// Fetch data from /api/products route
fetch('/api/products')
.then(response => {
return response.json();
})
.then(products => {
//Fetched product is stored in the state
setProducts(products);
});
};
useEffect(() => {
getProducts();
});
// Render the products
const renderProducts = () => {
//sort products from newest to oldest
let sortProducts = products.sort((a,b) => {
return b.id - a.id;
})
return sortProducts.map(product => {
return (
// handleClick() function is invoked onClick.
<li
key={product.id}
onClick={() => handleClick(product)}
>
{ product.title }
</li>
);
})
};
// Executes when user clicks list item, sets the state
const handleClick = (product) => {
setCurrentProduct(product)
};
// Add new product
const handleAddProduct = (product) => {
product.price = Number(product.price);
/*Fetch API for post request */
fetch( 'api/products/', {
method:'post',
/* headers are important*/
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(product)
})
.then(response => {
return response.json();
})
.then( data => {
//update the state of products and currentProduct
setProducts(prevProducts => prevProducts.concat(data))
setCurrentProduct(data)
})
};
//Delete product
//The delete button should ideally go inside the Product component
const handleDeleteProduct = () => {
const delProduct = currentProduct
fetch( 'api/products/' + currentProduct.id,
{ method: 'delete' })
.then(response => {
/* Duplicate the array and filter out the item to be deleted */
var newItems = products.filter(function(item) {
return item !== delProduct
});
setProducts(newItems)
setCurrentProduct(null)
});
};
//Update product
//The update feature should have a component of its own
const handleUpdateProduct = (product) => {
const updProduct = currentProduct;
fetch( 'api/products/' + currentProduct.id, {
method:'put',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(product)
})
.then(response => {
return response.json();
})
.then( data => {
/* Updating the state */
var updItems = products.filter(function(item) {
return item !== updProduct
})
setProducts(updItems.concat(product))
setCurrentProduct(product)
})
};
return(
<div>
<div class="boxes">
<ul>
{ renderProducts() }
</ul>
</div>
<div class="boxes">
<Product product={currentProduct} />
<AddProduct onAdd={handleAddProduct} />
</div>
</div>
)
}
export default Home;
|
Qui stiamo inizializzando lo stato dei prodotti su un array vuoto. Una volta montato il componente, useEffect
verrà eseguito. Al suo interno, usiamo fetch()
per recuperare i prodotti da /api/products e archiviarli nello stato. Definiamo quindi il metodo renderProducts
per descrivere l’interfaccia utente del componente. Tutti i prodotti vengono visualizzati come un elenco.
Abbiamo aggiunto currentProduct
nello stato e l’abbiamo inizializzato con il valore null
. La riga onClick={ () =>handleClick(product) }
richiama la funzione handleClick()
quando si fa clic su un prodotto. Il metodo handleClick()
aggiorna lo stato di currentProduct
.
Ora per visualizzare le informazioni del prodotto, possiamo eseguire il rendering all’interno del componente Home o creare un nuovo componente. Come accennato in precedenza, suddividere l’interfaccia utente in componenti più piccoli è il modo in cui React fa le cose, quindi creeremo un nuovo componente e lo chiameremo Product.
Il componente Product
è annidato all’interno del componente Home
. Il componente Home
passa il suo stato come props. Il componente Product accetta questi props come input e visualizza le informazioni pertinenti.
resources/assets/js/component/Product.js
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
|
import React from 'react';
/* Stateless component or pure component
* { product } syntax is the object destructing
*/
const Product = ({product}) => {
const divStyle = {
/*code omitted for brevity */
}
//if the props product is null, return Product doesn't exist
if(!product) {
return(<div style={divStyle}> <h3>Select a product from the list on the left</h3> </div>);
}
//Else, display the product data
return(
<div style={divStyle}>
<h2> {product.title} </h2>
<p> {product.description} </p>
<h3> Status {product.availability ? 'Available' : 'Out of stock'} </h3>
<h3> Price : {product.price} </h3>
</div>
)
}
export default Product ;
|
Aggiunta di un nuovo prodotto
Abbiamo implementato con successo il front-end corrispondente al recupero di tutti i prodotti e alla loro visualizzazione. Ora abbiamo bisogno di un formulario per aggiungere un nuovo prodotto. Il processo funzionerà in questo modo:
- Un nuovo componente stateful che renderizza l’interfaccia utente per un formulario. Lo stato del componente conterrà le informazioni inserite.
- Al momento dell’invio, il componente figlio passa lo stato al componente Home utilizzando un callback.
- Il componente Home ha un metodo
handleAddProduct()
che gestisce la logica per l’avvio di una richiesta POST. Dopo aver ricevuto la risposta, il componente Home aggiorna il proprio stato (siaproducts
checurrentProduct
).
resources/assets/js/component/AddProduct.js
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
import React, { useState } from 'react'
const AddProduct = (props) => {
const [newProduct, setNewProduct] =
useState(
{
title:"",
description: "",
price: 0,
availability: 0
}
);
const handleInput = (key, e) => {
/*Duplicating and updating the state */
var newState = Object.assign({}, newProduct);
newState[key] = e.target.value;
setNewProduct(newState);
};
const handleSubmit = (e) => {
//preventDefault prevents page reload
e.preventDefault();
//A call back to the onAdd props. The current state is passed as a param
props.onAdd(newProduct);
};
const divStyle = {
/*Code omitted for brevity */
}
return(
<div>
<h2> Add new product </h2>
<div style={divStyle}>
<form onSubmit={handleSubmit}>
<div>
<label> Title:
{ /*On every keystroke, the handeInput method is invoked */ }
<input type="text" onChange={(e)=>handleInput('title',e)} />
</label>
</div>
<div>
<label> Description:
<input type="text" onChange={(e)=>handleInput('description',e)} />
</label>
</div>
<div>
<label> Price:
<input type="number" onChange={(e)=>handleInput('price',e)} />
</label>
</div>
<div>
<label> Availability:
<input type="integer" onChange={(e)=>handleInput('availability',e)} />
</label>
</div>
<input type="submit" value="Submit" />
</form>
</div>
</div>
)
}
export default AddProduct
|
All’interno del componente Home
, abbiamo anche inserito <AddProduct />
:
1 |
<AddProduct onAdd={handleAddProduct} />
|
Il gestore di eventi onAdd
è concatenato al metodo handleAddProduct()
del componente. Questo metodo contiene il codice per effettuare una richiesta POST al server. Se la risposta indica che il prodotto è stato creato correttamente, lo stato di products
e currentProducts
viene aggiornato.
Ed ecco la versione finale dell’applicazione:
Cos’altro fare?
L’applicazione è incompleta senza le funzionalità di cancellazione e aggiornamento, ma se hai seguito attentamente il tutorial dovresti essere in grado di riempire questo vuoto senza problemi. Per iniziare, ti ho fornito la logica del gestore eventi per entrambi gli scenari.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
const handleDeleteProduct = () => {
const delProduct = currentProduct
fetch( 'api/products/' + currentProduct.id,
{ method: 'delete' })
.then(response => {
/* Duplicate the array and filter out the item to be deleted */
var newItems = products.filter(function(item) {
return item !== delProduct
});
setProducts(newItems)
setCurrentProduct(null)
});
};
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
const handleUpdateProduct = (product) => {
const updProduct = currentProduct;
fetch( 'api/products/' + currentProduct.id, {
method:'put',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(product)
})
.then(response => {
return response.json();
})
.then( data => {
/* Updating the state */
var updItems = products.filter(function(item) {
return item !== updProduct
})
setProducts(updItems.concat(product))
setCurrentProduct(product)
})
};
|
Dovresti tuffarti, sporcarti le mani e finire l’applicazione usando la logica di cui sopra! Ti darò un suggerimento: il pulsante Delete
dovrebbe idealmente andare all’interno del componente Product
, mentre la funzionalità Update
dovrebbe avere un componente proprio. Ti incoraggio ad accettare questa sfida e completare i componenti mancanti! Poi non ti dimenticare di condividerlo con noi :)
Riepilogo
Innanzitutto, abbiamo creato un’API REST utilizzando il framework Laravel. Poi abbiamo creato un front-end per l’API utilizzando React.
Sebbene ci siamo concentrati principalmente sulla creazione di un’applicazione a pagina singola utilizzando React, in realtà potresti creare widget o componenti che vengano montati su elementi specifici nelle tue visualizzazioni.
Negli ultimi anni, React è cresciuto in popolarità. In effetti, abbiamo un numero considerato di richieste di collaborazione e consulenze per questa tecnologia. Visita la nostra pagina dedicata Agenzia React e/o contattaci.
Hai già provato a sperimentare con Laravel e React? Quali sono i tuoi pensieri, idee, domande, opinioni? Lasciaci il tuo commento. E se ti è anche piaciuto il tutorial condividilo sui tuoi social, ti saremo molto grati :)