Tempo di lettura: 14 minuti
Nel videocorso React e Redux la guida completa abbiamo realizzato un’applicazione Stock Exchange con React e l’abbiamo chiamata Nasdaq. Questa è stata compilata mediante componenti statefull.
Nel corso di questo tutorial andremo a ricompilarla tutta mediante componenti stateless introdotti con gli hooks in React a partire dalla versione 16.8. Questi consentono di utilizzare lo stato e altre funzionalità della libreria senza scrivere una classe.
Anche se oggi si preferisce sviluppare con gli hooks ,quindi con metodi e non a classi, è consigliabile conoscere anche la parte statefull in quanto è molto probabile che vi troverete a modificare o a mantenere applicazioni React, un pò datate sviluppate con tale paradigma.
Applicazione Stock Exchange con React: scarica il progetto
Bene recatevi in una directory a vostro piacimento e da linea di comando digitate:
npx cerate-react-app nasdaq3.0.1
a download completato apritelo con il vostro ide preferito.
Modificate il componente App.js così:
import './App.css'; function App() { return ( <div className="App"> </div> ); } export default App;
visto che ci siamo creiamo anche gli altri; create la cartella components sotto src, quindi al suo interno create:
StockName,Search e Graphic e Stock.
Il componente Graphic.js
Iniziamo dal componente Graphic.js relativo alla visualizzazione del grafico :
import React from 'react'; const Graphic = () => { return ( <div> </div> ); }; export default Graphic;
ora è necessario, innanzitutto, scaricare il package recharts , quindi da terminale digitate:
npm install recharts --save
modificate il componente come di seguito:
import React from 'react'; import { LineChart, Line, CartesianGrid, XAxis, YAxis, Legend, Tooltip, ReferenceLine } from 'recharts'; const Graphic = ({ data, chiusura }) => { return ( <div> <LineChart width={600} height={400} data={data} margin={{ top: 45, left: 20, right: 30, bottom: 5 }}> <XAxis style={{ fontSize: '14px', color: 'green' }} dataKey="datetime" /> <YAxis style={{ fontSize: '14px' }} domain={[ dataMin => ((dataMin - dataMin * 10 / 100).toFixed(2)), dataMax => ((dataMax + dataMax * 10 / 100).toFixed(2)) ]} /> <CartesianGrid stroke="#eee" strokeDasharray="3 3" /> <Tooltip /> <Legend /> <ReferenceLine y={chiusura} label='' stroke='red' /> <Line type="monotone" dataKey="price" stroke="#82ca9d" activeDot={{ r: 8 }} /> </LineChart> </div> ); }; export default Graphic;
Il componente Search.js
Sempre sotto la folder components create il componente Search.js:
import React from 'react'; const Search = () => { return ( <div> </div> ); }; export default Search;
modificate come di seguito:
import React, { useState } from 'react'; const Search = ({ onInputSearch }) => { const [cerca, setCerca] = useState(''); const onInputChange = e => { setCerca(e.target.value); console.log(cerca); // Nota che `cerca` può non essere immediatamente aggiornato, quindi questo log potrebbe non mostrare l'ultimo valore. } const onSubmit = e => { e.preventDefault(); onInputSearch(cerca); setCerca(''); } const onFocus = e => { e.target.blur(); } return ( <div className='row'> <form className="form-inline" onSubmit={onSubmit}> <div className="form-group"> <input type="text" name="cerca" className="form-control" value={cerca} onChange={onInputChange} placeholder="Cerca..." /> </div> <button type='submit' onFocus={onFocus} className='btn btn-warning cercaButton'><i className="fas fa-search"></i></button> </form> </div> ); } export default Search;
Il componente StockName
Create il componente StockName.js sotto components:
import React from 'react'; const StockName = () => { return ( <div> </div> ); }; export default StockName;
modificate come di seguito:
import React from 'react'; const StockName = ({ onAddPreferiti, ids, data }) => { const addPreferiti = () => { onAddPreferiti(ids); }; return ( <div className='nomestock' onClick={addPreferiti}> <i className="fas fa-plus-circle"></i>{data.ticker} - {data.name} </div> ); }; export default StockName;
Il componente Stock.js
Realizzate il componente Stock.js sotto components:
import React from 'react'; const Stock = () => { return ( <div> </div> ); }; export default Stock;
modificatelo:
import React, { useState, useEffect } from 'react'; import './css/stock/stock.css'; import Graphic from './Graphic'; const token = process.env.REACT_APP_STOCK_API_TOKEN; const Stock = (props) => { const { dati,eliminoStock } = props; const { ticker, price } = dati; const [tickerData, setTickerData] = useState({ ticker, price }); const [datatrade, setDatatrade] = useState(new Date().toLocaleTimeString()); const [ckrealtime, setCkrealtime] = useState(''); const [datigrafico, setDatagrafico] = useState([ { datetime: new Date().toLocaleTimeString(), price: price }, ]); const [showgrafico, setShowgrafico] = useState(false); const [diff, setDiff] = useState(0); const [percentuale, setPercentuale] = useState(0); useEffect(() => { console.log('1f) FIGLIO Creo istanza'); }, []); useEffect(() => { if (ticker !== tickerData.ticker) { setTickerData({ ticker, price }); } }, [ticker, tickerData.ticker, price]); useEffect(() => { if (ckrealtime === 'checked') { const timer = setInterval(() => getNewElementi(), 10000); return () => { clearInterval(timer); }; } }, [ckrealtime]); const getNewElementi = () => { const url = `https://api.stockdata.org/v1/data/quote?symbols=${ticker}&api_token=${token}`; console.log(url); fetch(url) .then((r) => r.json()) .then((data) => { console.log('data', data); const tickerData = data.data[0]; const random = (Math.ceil(Math.random() * 10) * (Math.round(Math.random()) ? 1 : -1)) / 3; const datatrade = new Date().toLocaleTimeString(); console.log(datatrade); const dayOpen = tickerData.day_open; const newPrice = Number(dayOpen) + random; console.log(newPrice); const newDatagrafico = [ ...datigrafico, { datetime: datatrade, price: newPrice }, ]; setTickerData({ ticker, price: newPrice }); setDatatrade(datatrade); setDatagrafico(newDatagrafico); // Calcola la differenza tra il prezzo corrente e il prezzo di chiusura precedente const newDiff = (newPrice - dati.previous_close_price).toFixed(2); setDiff(newDiff); // Calcola la percentuale di variazione const newPercentuale = ((newDiff / dati.previous_close_price) * 100).toFixed(2); setPercentuale(newPercentuale); }) .catch((error) => { console.log('Fetch failed', error); }); }; const showGrafico = () => { setShowgrafico(!showgrafico); }; const disabled = datatrade >= '09:00:00' && datatrade <= '20:00:00' ? false : true; return ( <div className="stock col-md-6 p-3"> <div className="bodystock"> <i className="fas fa-times-circle closebtn" onClick={eliminoStock}></i> <div className="row"> <div className="col-sm"> <h2 className="giallo">{ticker}</h2> <p>Nasdaq</p> </div> <div className="col-sm"> <h2>{parseFloat(tickerData.price).toFixed(2)}</h2> <p className="giallo">{datatrade}</p> </div> <div className="col-sm"> <h2>{diff}</h2> <p style={{ color: percentuale < 0 ? 'red' : 'green' }}> {percentuale} % </p> </div> <div className="col-sm"> <p> <i className="fas fa-chart-line fa-2x" onClick={showGrafico} ></i> </p> <label className="bs-switch"> <input type="checkbox" checked={ckrealtime} onChange={() => setCkrealtime(ckrealtime === 'checked' ? '' : 'checked') } disabled={disabled} /> <span className="slider round"></span> </label> </div> </div> </div> <div className="bodygrafico"> <div className="row"> <div className="col-sm"> {showgrafico && <Graphic data={datigrafico} chiusura={dati.price} />} </div> </div> </div> </div> ); }; export default Stock;
chiaramente al momento non funziona in quanto dobbiamo ancora aggiungere css e soprattutto l’end point relativo allo stock exchange, ci arriveremo a breve.
Modifica del componente App.js
Ora che abbiamo realizzato i componenti necessari importiamoli in App.js, quindi modificate come di seguito:
import React, { useState, useEffect } from 'react'; import logo from './logo.png'; import '.App.css'; import Stock from './components/Stock'; import Search from './components/Search'; import StockName from './components/StockName' const token = process.env.REACT_APP_STOCK_API_TOKEN; const App = () => { const [listaelementi, setListaelementi] = useState([]); const [listapreferiti, setListapreferiti] = useState([]); const [inCaricamento, setInCaricamento] = useState(false); const [showError, setShowError] = useState(false); const [msg, setMsg] = useState(null); const [showAvviso, setShowAvviso] = useState(false); const [msgAvviso, setMsgAvviso] = useState(''); useEffect(() => { console.log('1g) il costruttore crea la prima istanza Genitore'); }, []); const cercaElementi = (str) => { console.log('Dati', listaelementi); getElementi(str); }; const getElementi = (str) => { const url = `https://api.stockdata.org/v1/data/quote?symbols=${str}&api_token=${token}`; setInCaricamento(true); setShowError(false); setShowAvviso(false); fetch(url) .then((r) => r.json()) .then((r) => { console.log('Risposta API', r); const data = r.data; // Assicurati che "data" sia l'array corretto console.log('Data:', data); if (Array.isArray(data)) { setListaelementi(data); setInCaricamento(false); console.log('Stato impostato correttamente'); } else { console.error('I dati ricevuti non sono un array'); } }) .catch((error) => { setInCaricamento(false); setShowError(true); setMsg(error.message); console.error('Fetch failed', error); }); }; const addPreferiti = (ids) => { // alert(`Hai cliccato sull'elemnto ${ids}`); setListapreferiti([...listapreferiti, listaelementi[ids]]); }; const eliminoStock = (symbol) => { const preferiti = listapreferiti.filter((el) => { if (el.symbol !== symbol) return true; return false; }); setListapreferiti(preferiti); }; console.log('2g) Genitore Render'); return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p style={{ color: 'gold' }}>Applicazione Stock Exchange</p> <Search onInputSearch={cercaElementi} /> <div className="container-fluid" style={{ marginTop: '20px' }}> <section className="col-md-12 listanomi"> <div className="row"> <div className="col"> {inCaricamento && <p className="text-center">Caricamento in corso ...</p>} {showError && <p className="text-center">{msg}</p>} {showAvviso && <p className="text-center">{msgAvviso}</p>} {Array.isArray(listaelementi) && listaelementi.length > 0 ? ( listaelementi.map((el, index) => ( <StockName key={index} data={el} ids={index} onAddPreferiti={addPreferiti} /> )) ) : ( <p>Nessun risultato trovato</p> )} </div> </div> </section> <section className="listapreferiti row"> {listapreferiti.map((el, index) => ( <Stock key={index} dati={el} eliminoStock={eliminoStock} symbol={el.symbol} /> ))} </section> </div> </header> </div> ); }; export default App;
Applicazione Stock Exchange con React: aggiungi gli stili , i font ed il logo
Ok non ci resta che aggiungere i fogli di stile, quelli che utilizzavamo nell’applicazione originaria, quindi aprite App.css e modificate:
body { background-color: #282c34; } .App { text-align: center; } .App-logo { /*animation: App-logo-spin infinite 20s linear;*/ height: 200px; } .App-header { display: flex; flex-direction: column; align-items: center; font-size: calc(10px + 2vmin); color: white; } .App-link { color: #61dafb; } .App input, .App textarea, .App select { display: inline; padding: 10px; font-size: 1.3rem; line-height: 1.2; color: #495057; background-color: #fff; background-clip: padding-box; border: 1px solid #ced4da; border-radius: .25rem; transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out; } .nomestock { padding: 10px; background-color: #CCC; margin: 5px; display: inline-block; border-radius: 5px; color: #000; font-size: 0.8em; cursor: pointer; } .bodystock { margin: 0 5px; font-size: 14px; padding: 10px; background-color: #464646; } .cercaButton{ padding:10px 13px } .closebtn { position: absolute; top: 20px; right: 25px; color: white; z-index: 999; cursor: pointer; } .stock { color: #FFF; margin-bottom: 5px; position: relative; } .giallo { color: #d8e707; } .bianco { color: #FFF; } .col p { color: #CCC; } a { color: #61dafb; text-decoration: none; } .bs-switch { position: relative; display: inline-block; width: 60px; height: 34px; } label { display: inline-block; margin-bottom: .5rem; } [type="checkbox"]:checked, [type="checkbox"]:not(:checked) { position: absolute; opacity: 0; pointer-events: none; } .bs-switch input { display: none; } input[type="checkbox"], input[type="radio"] { box-sizing: border-box; padding: 0; } .bs-switch input:checked+.slider { background-color: #2196f3; } .bs-switch .slider.round { -webkit-border-radius: 34px; border-radius: 34px; } .bs-switch .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; -webkit-transition: .4s; -o-transition: .4s; transition: .4s; } .bs-switch .slider.round::before { -webkit-border-radius: 50%; border-radius: 50%; } .bs-switch .slider::before { position: absolute; content: ""; height: 26px; width: 26px; left: 4px; bottom: 4px; background-color: #fff; -webkit-transition: .4s; -o-transition: .4s; transition: .4s; } .bs-switch input:checked+.slider::before { -webkit-transform: translateX(26px); -ms-transform: translateX(26px); transform: translateX(26px); }
quindi sotto components createvi le folder css/stock ed al loro interno aggiungete il file stock.css:
.stock { width: 700px; margin-bottom: 10px; padding: 12px; } .col { width: 25%; float: left; } .col p { color: #CCC; }
ora non ci resta che aggiungere il logo, prendetelo dalla vecchia applicazione ed incollatelo all’interno della folder src. (Per coloro che non hanno seguito il corso potete clonare il progetto dal mio repository).
Bene aprite il file index.html ed aggiugiamo i cdn di bootstrap e fontawesome, sono versioni meno recenti ma al momento non è il caso di stravolgere l’intera applicazione; difatti se andassimo ad aggiungere le ultime versioni di sicuro qualche cosa non tornerebbe in quanto , molto probabilmente, sono cambiate alcune classi del framework o del font.
Per velocizzare opto per l’inclusione via cdn e non via node, se volete provare ad includerle in node_modules fate pure (se non sapete come fare prendete spunto da questo articolo ad esempio):
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="Web site created using create-react-app" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <!-- manifest.json provides metadata used when your web app is installed on a user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ --> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <!-- Notice the use of %PUBLIC_URL% in the tags above. It will be replaced with the URL of the `public` folder during the build. Only files inside the `public` folder can be referenced from the HTML. Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.4.2/css/all.css" integrity="sha384-/rXc/GQVaYpyDdyxK+ecHPVYJSN9bmVFBvjA/9eOB+pb3F2w2N6fc5qB9Ew5yIns" crossorigin="anonymous"> <title>React App</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> <!-- This HTML file is a template. If you open it directly in the browser, you will see an empty page. You can add webfonts, meta tags, or analytics to this file. The build step will place the bundled scripts into the <body> tag. To begin the development, run `npm start` or `yarn start`. To create a production bundle, use `npm run build` or `yarn build`. --> </body> </html>
ora avviate il server digitando da terminale:
npm start
se tutto è andato a buon fine questo è il risultato:
tuttavia se provate a fare una ricerca di sicuro non troverà nulla in quanto ci manca ancora di implementare correttamente la chiamata all’api.
Implementa l’api di StockData.org
Navigate sul sito di Stockdata quindi cliccate su pricing e scegliete di registrare un piano free(ammesso che qualcuno di voi non voglia pagare switchando i vari piani):
una volta seguito la procedura vi verrà assegnato un token, salvatevelo da qualche parte, inoltre vi ricordo che con il piano gratuito siamo limitati come potete notare, ad esempio possiamo fare massimo 100 richieste giornaliere, comunque per scopo didattico basta ed avanza.
Ritorniamo all’applicazione dove andremo a salvare il token all’interno di un file environment, per far ciò scarichiamoci il package dotenv via node, da terminale digitate:
npm i dotenv
una volta fatto create il file .env nella directory principale del progetto e digitate quanto segue:
DEBUG=true REACT_APP_STOCK_API_TOKEN=il vostro token
per poter leggere le proprietà del file appena creato modificate il file index.js, ossia l’entry point dell’applicazione come segue:
import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import dotenv from 'dotenv'; dotenv.config(); const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <App /> </React.StrictMode> ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals();
come vedete molto semplicemente importo la libreria dotenv e poi la richiamo:
... import dotenv from 'dotenv'; dotenv.config(); ...
Ora dovrebbe essere tutto a posto, provate a riavviare il server ed a fare una ricerca, purtroppo con il piano free possiamo richiedere solo aapl,tsla,msft:
funziona anche il grafico informativo.
Bene si conclude questo tutorial, chi ha svolto il corso React e Redux: la guida completa evidentemente sarà stato facilitato nel seguire questa mini guida in quanto avevano già realizzato la web app, per gli altri se volete lo potete acquistare su Udemy, se non siete soddisfatti avete 30gg di tempo per chiedere il rimborso.
Potete clonare il progetto da questo git se volete.
Se avrete voglia di seguire gli altri tutorial ne sarò felice 😎😎😎
Se volete Approfondire le conoscenze su REACT E NODEJS CON EXPRESS E MONGODB potete seguire il corso completo su Udemy sempre scontato a 9.99/14.99 € , inoltre, nel momento in cui non foste soddisfatti, avete la possibilità di ottenere il rimborso completo dello stesso entro 30 giorni dalla data di acquisto!