In questo articolo parliamo delle context API ufficiali, come usarle e cosa sono. Sono senza dubbio molto importanti e sono certo che se già conosci ReactJS non ti sfuggirà il vantaggio nel loro utilizzo. Ti ricordo infatti che solo da ReactJS 16.3 sono disponibili le context API ufficiali, mentre prima potevi utilizzare solo quelle sperimentali.
Le context API ufficiali, come usarle e cosa sono
ReactJS ha sempre offerto delle context API sperimentali e finalmente dalla versione 16.3 di ReactJS sono state introdotte le context API ufficiali. Con ReactJS 16.3 sono state introdotte diverse novità che abbiamo già visto in questi articoli:
- Il componente StrictMode
- I nuovi metodi del lifecycle getDerivedStateFromProps e getSnapshotBeforeUpdate
In vista della versione 17 infatti il team di Facebook si sta preparando a diversi cambiamenti e tra questi le context API ufficiali rappresentano uno dei più importanti.
Vediamo quindi insieme le context API ufficiali, come usarle e cosa sono.
Cosa è il context?
Tramite il context è possibile passare dei valori da un componente ad un altro senza dover specificare le props su ogni componente. Come ben sai infatti ReactJS funziona normalmente passando le props da un componente padre ai suoi figli.
Ma cosa succede se ci sono proprietà che dovrebbero essere passate a molti componenti figli? Pensa per esempio alla lingua scelta dall’utente o ad un tema scelto dall’utente, ogni componente dovrà prendere una prop che sarà stata passata da un componente di livello più alto e così via. Si creerebbero tantissimi componenti tutti con la stessa proprietà che viene passata dall’alto verso il basso: A > B > C > D > E > F > G > H > I, ecc.
Per ovviare a questo problema qualcuno potrebbe dire che lo store di Redux sia un ottimo posto dove salvare certe informazioni, ma nella realtà dei fatti Redux non è sempre la soluzione a tutti i problemi ed a volte il suo utilizzo non è richiesto. La soluzione migliore è senza dubbio quella di usare il context (girano già voci per le quali l’utilizzo del context sia anche un’alternativa completa all’utilizzo di Redux, ma personalmente non voglio trattare l’argomento in questa sede e vorrei capire di quale complessità si parla).
Riassumendo: grazie alle context API ufficiali hai la possibilità di non usare le props passando dati come un tema o la lingua scelta di componente in componente, ma hai la possibilità di condividere dei valori tra componenti senza dover passare delle props esplicitamente.
Come usare le context API?
Per capire le context API ufficiali, come usarle e cosa sono potrebbe non bastarti la spiegazione appena vista. Quindi ora vediamo un esempio insieme per farti capire meglio quanto detto.
Le context API mettono a disposizione il metodo createContext:
const {Provider, Consumer} = React.createContext(defaultValue);
CreateContext è un metodo di React che prende come parametro un valore di default e ritorna un Provider ed un Consumer.
Ogni volta che un Consumer viene utilizzato senza che abbia come padre un Provider allora avrà a disposizione il valore di default, altrimenti sarà il Provider a fornire un valore.
Il Provider può essere utilizzato in questo modo:
<Provider value={/* some value */}> .... </Provider>
Prende un value come prop ed al suo interno possono essere inseriti tutti quei componenti di cui la web app necessita. Se un Provider contiene a sua volta un Provider con altri componenti figli allora il valore che vince è quello del Provider più profondo nell’albero dei componenti.
Il Consumer deve essere utilizzato così:
<Consumer> {value => /* render something based on the context value */} </Consumer>
Al suo interno è possibile usare un valore e renderizzare altri componenti in base a quel valore. Di base richiede quindi una funzione come figlio che riceve il valore del context e ritorna un nodo React.
Ogni Consumer viene renderizzato nuovamente ogni volta che la prop del Provider cambia e non viene interpellato il metodo del lifecycle shouldComponentUpdate.
Un esempio
Supponiamo di avere un tema nella nostra web app che può essere cambiato dall’utente nella versione light e dark. Senza le context API magari avresti passato una props in tutti i componenti figli per decidere quali classi CSS e stili utilizzare.
Ecco come avresti fatto:
class App extends React.Component { render() { return <Toolbar theme="dark" />; } } function Toolbar(props) { // The Toolbar component must take an extra "theme" prop // and pass it to the ThemedButton. This can become painful // if every single button in the app needs to know the theme // because it would have to be passed through all components. return ( <div> <ThemedButton theme={props.theme} /> </div> ); } function ThemedButton(props) { return <Button theme={props.theme} />; }
Il tuo componente App avrebbe renderizzato una toolbar passando un tema, che a sua volta la toolbar avrebbe passato al componente del pulsante che a sua volta l’avrebbe passato al pulsante. E pensa poi a tutti gli altri componenti: magari la toolbar avrebbe anche qualche scritta (colore nero su sfondo light e colore bianco su sfondo dark), magari qualche altro pulsante per un menù a tendina, poi la home con altri menù e informazioni, il footer, ecc..
Per ovviare a questo problema in cui una prop viene passata di componente in componente e per tutte quelle props dove shouldComponentUpdate non deve essere interpellato, ci viene in aiuto la nuova context API:
// Context lets us pass a value deep into the component tree // without explicitly threading it through every component. // Create a context for the current theme (with "light" as the default). const ThemeContext = React.createContext('light'); class App extends React.Component { render() { // Use a Provider to pass the current theme to the tree below. // Any component can read it, no matter how deep it is. // In this example, we're passing "dark" as the current value. return ( <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> ); } } // A component in the middle doesn't have to // pass the theme down explicitly anymore. function Toolbar(props) { return ( <div> <ThemedButton /> </div> ); } function ThemedButton(props) { // Use a Consumer to read the current theme context. // React will find the closest theme Provider above and use its value. // In this example, the current theme is "dark". return ( <ThemeContext.Consumer> {theme => <Button {...props} theme={theme} />} </ThemeContext.Consumer> ); }
In questo esempio banale App contiene il Provider e tutti i componenti figli, se necessario, possono essere Consumer. Come puoi vedere il componente Toolbar non ha la necessità di conoscere il context e propagare a sua volta il tema della web app. La toolbar non ha interesse nel tema, bensì hanno interesse i componenti che nascono per avere uno stile appropriato al tema come ThemedButton.
Da questo momento in poi ThemedButton può essere utilizzato da chiunque:
- Componenti che si trovano all’interno di un Provider
- Componenti che si trovano all’interno di più Provider
- Componenti che non hanno interesse nel tema e non si trovano all’interno di un Provider (il Consumer utilizzerà il valore di default del context)
Abbiamo scoperto le context API ufficiali, come usarle e cosa sono ed adesso vediamo insieme come poter cambiare il valore di un context anche da componenti figli.
Cambiare il context da un componente
Per cambiare il valore di un context da un componente c’è la possibilità di creare un context in questo modo:
// Make sure the shape of the default value passed to // createContext matches the shape that the consumers expect! export const ThemeContext = React.createContext({ theme: themes.dark, toggleTheme: () => {}, });
In questo caso themes è un oggetto contenente due proprietà, light e dark, che sono a loro volta oggetti contenenti dello stile. Di default toggleTheme non fa nulla.
Di conseguenza il componente di alto livello, come App, può implementare una logica di questo tipo:
import {ThemeContext, themes} from './theme-context'; import ThemeTogglerButton from './theme-toggler-button'; class App extends React.Component { constructor(props) { super(props); this.toggleTheme = () => { this.setState(state => ({ theme: state.theme === themes.dark ? themes.light : themes.dark, })); }; // State also contains the updater function so it will // be passed down into the context provider this.state = { theme: themes.light, toggleTheme: this.toggleTheme, }; } render() { // The entire state is passed to the provider return ( <ThemeContext.Provider value={this.state}> <Content /> </ThemeContext.Provider> ); } } function Content() { return ( <div> <ThemeTogglerButton /> </div> ); } ReactDOM.render(<App />, document.root);
Ovvero definisce una function toggleTheme che è in grado di impostare nello state il tema scelto. Lo state è quindi composto da un tema (themes.light) e dalla function toggleTheme. Il Provider all’interno del metodo render prende come prop value che non è altro che lo state di App.
A questo punto ThemeTogglerButton sarà così implementato:
import {ThemeContext} from './theme-context'; function ThemeTogglerButton() { // The Theme Toggler Button receives not only the theme // but also a toggleTheme function from the context return ( <ThemeContext.Consumer> {({theme, toggleTheme}) => ( <button onClick={toggleTheme} style={{backgroundColor: theme.background}}> Toggle Theme </button> )} </ThemeContext.Consumer> ); } export default ThemeTogglerButton;
ThemeTogglerButton è un Consumer di ThemeContext ed ha la possibilità di creare un bottone per il cambio del tema. Al click viene chiama toggleTheme, definita in App, che controlla lo state corrente ed imposta un nuovo tema. Il cambio di state in App stimola il nuovo render e quindi il Provider avrà come prop value un oggetto differente (dal tema light a quello dark). Il bottone stesso che ha chiamato toggleTheme ha uno stile definito in base al tema.
Informazioni utili aggiuntive
Credo che ormai hai capito bene le context API ufficiali, come usarle e cosa sono.
Ti dò però qualche informazioni aggiuntiva:
- Hai la possibilità di avere diversi Provider annidati anche relativi a context differenti
- Devi stare attento al valore che usi come prop del Provider
Per quanto riguarda la possibilità di avere diversi Provider annidati anche di context differenti vuol dire che puoi fare questo:
<ThemeContext.Provider value={theme}> <UserContext.Provider value={signedInUser}> <Layout /> </UserContext.Provider> </ThemeContext.Provider>
Layout può essere quindi Consumer di entrambi (capisci perché in molti pensano che forse sia meglio, anche per leggerezza, usare le context API al posto di Redux?).
Per quanto riguarda il valore che usi come prop ricordati che il context utilizza le referenze per determinare quando lanciare un nuovo render. Vediamo questo esempio:
class App extends React.Component { render() { return ( <Provider value={{something: 'something'}}> <Toolbar /> </Provider> ); } }
Ogni volta che App viene renderizzata value diventa un nuovo oggetto e quindi tutti i Consumer vengono renderizzati nuovamente. Supponiamo che App subisca un cambio di stato che non interessa ai componenti figli (che magari implementano anche shouldComponentUpdate) allora come facciamo per far sì che i Consumer non vengano interessati di questo cambio di stato? In questo semplice modo:
class App extends React.Component { constructor(props) { super(props); this.state = { value: {something: 'something'}, }; } render() { return ( <Provider value={this.state.value}> <Toolbar /> </Provider> ); } }
Se non cambia value nello state di App il Provider non subisce cambiamenti e non obbliga i Consumer a renderizzarsi nuovamente.
Ci sono semplificazioni? Sì!
Per non scrivere in ogni componente il Consumer e la sua funzione puoi risparmiare tempo creando un componente di alto livello chiamato withTheme:
const ThemeContext = React.createContext('light'); // This function takes a component... export function withTheme(Component) { // ...and returns another component... return function ThemedComponent(props) { // ... and renders the wrapped component with the context theme! // Notice that we pass through any additional props as well return ( <ThemeContext.Consumer> {theme => <Component {...props} theme={theme} />} </ThemeContext.Consumer> ); }; }
WithTheme prende un componente e ritorna un altro componente che fa da wrapper. Il componente in input infatti non fa altro che essere incapsulato all’interno del Consumer e ricevere oltre alle props definite anche la prop relativa al tema.
Ecco un esempio:
function Button({theme, ...rest}) { return <button className={theme} {...rest} />; } const ThemedButton = withTheme(Button);
Conclusioni
In questo articolo hai imparato le context API ufficiali, come usarle e cosa sono. Ti ricordo che per maggiori dettagli puoi sempre guardare la guida ufficiale.
Non scordarti che se hai bisogno di un ripasso su ReactJS c’è la guida per imparare ReactJS. Inoltre ho scritto anche una guida che ti insegna a creare la tua prima app per Android e iOS con React Native, la trovi quì.
Per approfondire React potrebbero esserti utili alcuni libri quali:
Ed alcuni corsi Udemy quali:
- Introduction to TypeScript DevelopmentCorso JavaScript – ES6, NodeJS, ReactJS in italiano (Lite)
- Modern React with Redux
- React 16 – The Complete Guide (incl. React Router 4 & Redux)
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!).
complimenti