In questo articolo vediamo la creazione delle prime pagine con React, TypeScript e Redux. Ti ricordo che questo articolo fa parte di una guida ben più ampia che puoi trovare a questo indirizzo. Nella guida precedente abbiamo fatto un po’ di installazioni, adesso iniziamo a scrivere un po’ di codice ed a creare i nostri primi componenti.
React, TypeScript e Redux: creazione delle prime pagine
Andiamo in ordine, quello che vogliamo creare è una web app che consenta ad un utente di effettuare il login, consultare una dashboard e modificare il suo profilo. Questa web application è molto banale, non utilizza servizi esterni, ma senza dubbio ti permette di capire molto bene come funzionano Redux e TypeScript.
L’app ha più pagine quindi come nella guida su ReactJS introduciamo il routing così da favorire la navigazione.
Il file Index
Per prima cosa nella cartella src dovresti avere il file index.tsx. All’interno di questo file inseriamo il codice per renderizzare la nostra app:
import * as React from 'react'; import * as ReactDOM from 'react-dom'; import App from './App'; import './index.css'; ReactDOM.render( <App />, document.getElementById('root') as HTMLElement );
App
Visto che in index.tsx stiamo renderizzando App dobbiamo modificare il file App.tsx (se non c’è già crealo in src). Il file deve avere questo codice:
import * as React from 'react'; import './App.css'; import Router from "./components/routing/router"; class App extends React.Component { render() { return ( <div className="App"> <Router/> </div> ); } } export default App;
In questo file non facciamo altro che eseguire il render di un div contenente il router.
Il Router Component
Per proseguire crea in src la cartella components. All’interno di components crea la cartella routing ed infine all’interno della cartella routing crea il file router.tsx. All’interno di questo file inserisci:
import * as React from "react"; import { BrowserRouter, Route, Switch} from 'react-router-dom'; import { Provider } from 'react-redux'; import MainTemplate from "../../containers/templates/mainTemplate"; import Home from '../../containers/home/home'; import UserProfile from "../../containers/profile/userProfile"; import Dashboard from "../../containers/dashboard/dashboard"; import {getStore} from '../../store/storeCreator'; const store = getStore(); export default class RouterComponent extends React.Component<any, any>{ render(){ return ( <Provider store={store}> <BrowserRouter> <Switch> <MainTemplate> <Route exact={true} path='/' component={Home}/> <Route exact={true} path='/profile' component={UserProfile}/> <Route exact={true} path='/dashboard' component={Dashboard}/> </MainTemplate> </Switch> </BrowserRouter> </Provider> ); } }
Stiamo importando diverse cose che potresti non conoscere e sulle quali a breve ci soffermiamo. Questo componente non fa altro che renderizzare le route della web app che sarà divisa in home, profile e dashboard. Tutte le pagine renderizzate sono sempre contenute in un template principale che definisce header e footer.
React Router Dom
Per prima cosa viene importato react-router-dom che ci fornisce la route e tutto quanto già visto in questo articolo.
Procediamo con la sua installazione posizionandoci con il terminale nella root del progetto e scrivendo:
yarn add react-router-dom @types/react-router-dom
Il Provider di react-redux
Dunque importiamo il Provider di react-redux che ci serve per poter utilizzare lo store (lo vediamo tra poco) in tutti i componenti che utilizzano connect (vediamo anche questo in questa guida, per adesso non ti preoccupare).
Il Provider non fa altro che aiutarci durante i test e fare in modo di non dover passare lo store nei vari componenti tramite le props.
GetStore
Importiamo dunque getStore, una funzione che va a creare il nostro store. La nostra app si concentra sull’utente connesso quindi lo store ora conterrà il nostro utente.
All’interno di src crea la cartella store ed al suo interno il file storeCreator.ts (l’estensione tsx non è necessaria per i file dove non scriviamo HTML). In questo file inserisci questo codice:
import { createStore } from 'redux'; import { user } from '../reducers/index'; import { StoreState } from '../types/index'; import * as actions from "../actions"; export const defaultUser = { // Usiamo const così da avere defaultUser disponibile anche nei tests.. name:'', surname:'', age:0, mail:'', nickname:'' }; /* An important point to always remember is that when using combineReducers, the value returned from each reducer is not the state of the application. It is only the value of the particular key they represent in the state object! For example, the user reducer returns the value for the user key in the state. Likewise, the messages reducer returns the value for the messages key in the state. combineReducers(user, messages, contact) is like combineReducers(user:user, messages:messages, contact:contact) where user is an object in the store, messages is an object in the store, etc. */ const store = createStore<StoreState, actions.UserAction, any, any>(user, { user:defaultUser }); export function getStore(){ return store; }
I commenti ti potrebbero essere di aiuto più avanti, in ogni caso non facciamo altro che definire un utente di default, creare uno store tramite createStore di redux e ritornarlo tramite la function getStore.
Nota: createStore prende in ingresso un reducer e lo stato iniziale. Il reducer è l’aiutante che sa cosa deve fare quando eseguiremo il dispatch di un’ azione. CreateStore con TypeScript viene definito con l’utilizzo del tipo StoreState, actions.UserAction e dunque any (maggiori informazioni quì). Non ci soffermiamo troppo su questi dettagli al momento che potresti fare confusione.
Reducer
Dunque dobbiamo creare il nostro reducer, ovvero colui che saprà modificare lo store una volta che gli vengono inviate delle azioni tramite dispatch.
All’interno di src crea la cartella reducers e dunque il file index.tsx. Al suo interno scrivi:
import { UserAction } from '../actions'; import { StoreState } from '../types/index'; import { LOGOUT_USER, LOGIN_USER, EDIT_USER } from '../constants/index'; import {defaultUser} from '../store/storeCreator'; export function user(state: StoreState, action: UserAction): StoreState { switch (action.type) { case LOGOUT_USER: return {...state, user:defaultUser}; case LOGIN_USER: case EDIT_USER: return {...state, user:action.value}; } return state; }
Lo user visto nel paragrafo precedente importato da reducers/index è una funzione che riceve come parametri uno state e una action. Al suo interno viene implementato uno switch che in base al valore di action.type esegue un’azione. In questo caso specifico al logout ripristiniamo l’utente di default (importato da storeCreator) ed in caso di login o edit andiamo a modificare l’utente.
Le Actions
Dunque abbiamo visto che il reducer necessita delle actions per definire cosa deve accadere e come modificare lo store. Tali action le definiamo andando a creare il file src/actions/index.tsx con il seguente contenuto:
// Constants import * as constants from '../constants' // Interfaces import {User} from "../interfaces"; export interface LogoutUser { type: constants.LOGOUT_USER; } export interface LoginUser { type: constants.LOGIN_USER; value: User; } export interface EditUser { type:constants.EDIT_USER; value: User; } export type UserAction = LogoutUser | LoginUser | EditUser; export function logoutUser() : LogoutUser { return { type:constants.LOGOUT_USER } } export function loginUser(value: User) : LoginUser { console.log("Actions :: loginUser called with value", value); return { type:constants.LOGIN_USER, value } } export function editUser(value: User) : EditUser { console.log("Actions :: editUser called with value", value); return { type:constants.EDIT_USER, value } }
In ordine:
- Importiamo delle costanti
- Importiamo l’interfaccia User
- Creiamo le interfacce LogoutUser, LoginUser ed EditUser. Queste interfacce definiscono come le action verrano inviate al reducer (come puoi vedere in caso di login ed edit abbiamo la necessità di avere un valore che rispecchi l’interfaccia User)
- Dunque creiamo un type da esportare, UserAction. Questo tipo è definito dalle tre interfacce appena create
- Infine creiamo delle funzioni che ritornano un oggetto che rispecchi l’interfaccia definita sopra. Per esempio nel caso di editUser questa funzione riceve un parametro che rispecchia l’interfaccia User e ritorna un oggetto che rispecchia l’interfaccia EditUser, ovvero che contiene sia type che value
Le costanti
Le costanti le creiamo in src/constants/index.tsx:
export const LOGOUT_USER = 'LOGOUT_USER'; export type LOGOUT_USER = typeof LOGOUT_USER; export const LOGIN_USER = 'LOGIN_USER'; export type LOGIN_USER = typeof LOGIN_USER; export const EDIT_USER = 'EDIT_USER'; export type EDIT_USER = typeof EDIT_USER;
Come puoi vedere creiamo una costante e per ognuna un tipo da esportare. Il type lo utilizziamo con le actions nella definizione delle interfacce ed il value lo utilizziamo nelle action quando definiamo le funzioni e ritorniamo l’oggetto.
L’interfaccia User
Questa interfaccia definisce come deve essere l’utente e quali proprietà deve avere l’oggetto che andiamo a modificare. In src/interfaces/index.tsx andiamo a scrivere:
export interface User{ name: string, surname: string, mail: string, nickname: string, age: number }
Il nostro utente avrà nome, cognome, mail, nick e un’età. Questi campi dovranno rispettare i tipi imposti in questa interfaccia.
Il tipo StoreState
Vediamo dunque cos’è StoreState che utilizziamo nel nostro reducer. All’interno di src/types/index.tsx andiamo a scrivere:
// Interfaces import {User} from "../interfaces"; export interface StoreState { user: User; }
StoreState è un’interfaccia. Chi la implementa deve contenere un oggetto user che rispetti a sua volta l’interfaccia User.
Nota: se un giorno ci saranno anche altri oggetti nello store avremo anche altre proprietà che rispetteranno altre interfacce.
Il MainTemplate
Il MainTemplate non fa altro che renderizzare i componenti figli. E’ un wrapper che ci consente di avere in tutti i componenti, senza doverlo ripetere, un header e un footer.
Sempre in src crea la cartella containers ed al suo interno la cartella templates. All’interno di quest’ultima crea il file mainTemplate.tsx e scrivi al suo interno:
import MainTemplate, {StateProps, DispatchProps} from '../../components/templates/mainTemplate'; import * as actions from '../../actions/'; import { StoreState } from '../../types/index'; import { connect, Dispatch } from 'react-redux'; export function mapStateToProps({ user }: StoreState, props:any) : StateProps { return { user } } export function mapDispatchToProps(dispatch: Dispatch<actions.UserAction>) : DispatchProps { return { onUserLogout: () => dispatch(actions.logoutUser()), } } export default connect(mapStateToProps, mapDispatchToProps)(MainTemplate);
Questo componente è un componente container, il ruolo dei file che inseriremo nella cartella containers è quello di preparare alcuni dati, lavorare con lo store e redux e dunque renderizzare un componente (è solo uno dei mille modi per strutturare il progetto e per questa guida volevo utilizzare questo pattern suggerito da Dan Abramov).
Ecco cosa stiamo facendo:
- Importiamo il componente MainTemplate e due interfacce (le vediamo a breve)
- Importiamo delle actions
- Dunque importiamo un tipo, StoreState
- Infine importiamo connect e il tipo Dispatch da react-redux
- Creiamo una function mapStateToProps. Questa function riceve in ingresso lo store e le props del componente e ritorna delle StateProps. TypeScript necessita di avere la dichiarazione dei tipi sia per i parametri che per il tipo di ritorno di un metodo/funzione. In questo caso stiamo dichiarando che riceviamo un oggetto di tipo StoreState e vogliamo la proprietà user al suo interno, che le props possono essere di qualsiasi tipo e che verrà ritornato un oggetto che rispetta il tipo StateProps.
- Quindi creiamo mapDispatchToProps. Questa function riceve come parametro dispatch che è di tipo Dispatch<actions.UserAction> e ritorna delle DispatchProps. Questa funzione ci ritorna quindi delle funzioni da utilizzare nel componente renderizzato tramite le props.
- Infine esportiamo utilizzando la connect il componente MainTemplate (che dobbiamo ancora creare). Grazie a connect il componente MainTemplate riceverà nelle sue props user e onUserLogout e potrà utilizzarli e rimanere in ascolto di eventuali cambiamenti.
Il componente MainTemplate
Il componente MainTemplate che viene renderizzato a partire dal suo container MainTemplate lo creiamo in src/components/templates/mainTemplate.tsx. Al suo interno scriviamo:
import * as React from "react"; import { withRouter } from 'react-router-dom'; import { RouteComponentProps } from 'react-router-dom'; // Interfaces import {User} from "../../interfaces"; // Components import Header from "../header/header"; import Footer from "../footer/footer"; export interface StateProps{ user?: User; } export interface DispatchProps { onUserLogout: () => void } class MainTemplate extends React.Component<StateProps & DispatchProps & RouteComponentProps<any>, any>{ render(){ console.log("MainTemplate :: user is", this.props.user); return( <div> <Header user = {this.props.user} history = {this.props.history}/> <div className = "container-fluid"> <div className = "main content"> {this.props.children} </div> </div> <Footer/> </div> ); } } export default withRouter(MainTemplate); // Viene renderizzato così come se fosse una route e posso avere le props di una Route (come history)
In questo componente non facciamo altro che:
- Esportare l’interfaccia StateProps (se ti ricordi la importiamo nel container per definire il tipo di ritorno di mapStateToProps). Nota: user ha un punto di domanda (user?:..), ciò vuol dire che user potrebbe essere anche null o undefined.
- Esportare l’interfaccia DispatchProps (utilizzata nel container come tipo di ritorno per mapDispatchToProps)
- Creiamo dunque una classe MainTemplate che si aspetta di lavorare con oggetti StateProps, DispatchProps e RouteComponentProps
- Dunque andiamo a renderizzare i componenti Header, i children (che saranno le nostre routes) ed il Footer
- Infine esportiamo MainTemplate utilizzando withRouter. Grazie a withRouter importata da react-router-dom possiamo renderizzare MainTemplate con le stesse props di un componente che si trova dentro ad una route (nel nostro caso ci serve infatti la history da passare al componente Header)
Nel prossimo articolo..
Nel prossimo articolo vediamo Header e Footer e dunque le altre pagine. Capiremo con il codice sotto mano come funziona redux perché andremo ad eseguire un dispatch ed entreremo nel dettaglio di quanto visto fino ad ora.
So che se non conosci Redux diventa complicato impararlo insieme a TypeScript. Come avrai capito TypeScript ci permette di definire i tipi dei parametri ed i tipi di ritorno di metodi e funzioni. TSLint ci obbliga a farlo restituendoci degli errori in caso non rispettassimo le regole impostate. Ovviamente credo che, se scegli TypeScript, vuoi anche abituarti ad usarlo per bene e quindi a definire sempre i tipi di dato.
Ovviamente TypeScript fa anche molto di più come puoi vedere quì. Al momento però cerchiamo di procedere per gradi anche perché, per quanto mi riguarda, un uso massivo di TypeScript con React trasforma il progetto in qualcosa di meno divertente ed inoltre introduce tanto codice in più. Personalmente al momento lo utilizzerei solo per definire i tipi nelle classi che definiscono il modello, per aiutarsi con i parametri delle funzioni e per il loro tipo di ritorno.
Redux al contrario da un certo punto di vista è molto meno macchinoso: ti è sufficiente avere uno store che viene creato con createStore. Questo metodo necessità di un reducer, ovvero colui che sa come modificare lo store, e dello state iniziale. Il reducer non fa altro che ricevere delle azioni che sono oggetti contenenti la proprietà type ed altri valori. Grazie a type il reducer sa che azione si vuole performare. Infine le actions non sono altro che degli oggetti che contengono almeno il tipo, ma volendo anche altri valori. Nel prossimo articolo vedremo come, chiamando dispatch, invieremo un’azione al reducer che aggiornerà il nostro store.
Mentre attendi potrebbero esserti utili alcuni libri quali:
Ed alcuni corsi Udemy quali:
- Introduction to TypeScript Development
- Corso JavaScript – ES6, NodeJS, ReactJS in italiano (Lite)
- Modern React with Redux
- React 16 – The Complete Guide (incl. React Router 4 & Redux)
Non ti perdere anche la guida su React Native.
Se vuoi rimanere aggiornato sugli articoli del blog ti consiglio di iscriverti alla newsletter. Mando da 1 a 4 mail al mese e normalmente invio risorse gratuite e riservate solo agli iscritti. Invio anche la lista degli articoli di maggiore impatto, come questo. Se non troverai gli articoli potrai recuperarli dalla mail in questo modo ?
Per dubbi o domande non esitare a scrivermi nei commenti ?
Se ti è piaciuto l’articolo seguimi su Facebook e Twitter oppure rimani sempre aggiornato con la newsletter (da 1 a 4 mail al mese!).