Utveckling & Arkitektur

Skrivet 2019-06-05

Innan vi sätter igång

I det här blogginlägget kommer jag att gå igenom de fundamentala grunderna till JavaScript-biblioteket Redux och förklara när och hur det är som mest lämpligt att applicera det på din applikation. För attRedux du i slutet av den här genomgången ska ha fått en förståelse kring Redux, som du sedan kan bygga vidare på egen hand, avgränsar vi oss och bortser från att prata om associerade bibliotek till Redux, såsom Redux-Thunk, Redux-Saga, Reselect samt Recompose. I det här inlägget fokuserar jag endast på grunderna och varför vi bör använda Redux.

En förutsättning för att förstå sig på Redux är att ha en grundläggande kunskap inom JavaScript och ES6. Att även förstå sig på något modern JavaScript-ramverk, förslagsvis React.js och ha en utvecklingsmiljö redo är att rekommendera. Detta kan du förslagsvis göra med hjälp av Create React App som är ett verktyg som snabbt hjälper dig att komma igång med att skriva applikationer med hjälp av React. För att få en grundläggande genomgång av React och vad faktiskt Create React App skapar upp i bakgrunden föreslår jag starkt att du läser igenom min kollega, Pontus, bloggserie om hur du kickstartar din Front-end med React här. Du kan självklart sätta upp din utvecklingsmiljö med React, webpack och Babel om du vill det.

Introduktion

Om du någon gång har byggt en webbapplikation har du troligtvis beskrivit och hanterat olika tillstånd – såsom att visa upp en spinner medan applikationen försöker hämta data om nästkommande matcher i en fotbollsturnering, presentera de produkter du lagt in i en inköpslista eller kanske hålla koll på poängställningen i något spel du byggt. Gemensamt för samtliga är att ett tillstånd, även kallat state, för vart applikationen befinner sig just i detta nu beskrivs. Även den mest simpla JavaScript-applikationen har ett state.

Tänk dig följande scenario:
Du har en knapp.
När användaren trycker på knappen syns ett meddelande.
Hur simpelt det ens kan tyckas har vi här ett state att ta hänsyn till.

Vårt initiala state ser ut som följande JavaScriptobjekt:

var state = { 
buttonClicked: ’no’,
showText: ’no’
}

När användaren trycker på knappen får vi följande:

var state = { 
buttonClicked: ’yes’,
showText: ’yes’
}

Att hålla koll på en sådan ändring kan ses som relativt enkelt, men när vi har en applikation som växer och där flertalet states ändras ofta och vi vill ha en överblick över vad som sker samt vara säkra på att vårt state är intakt kan vi ta hjälp av olika metoder - förslagsvis Redux.

Vad är Redux

 

"Redux is a predictable state container for JavaScript apps."

Så lyder den officiella förklaringen för vad Redux innebär. Redux är alltså att JavaScript-bibliotek som hjälper oss att hålla koll på applikationens olika state och hur de förändras över tid. Redux hjälper även dig som utvecklare att debugga, återspela olika actions som dispatchas (mer om detta senare) eller varför inte kolla på diagram över hur applikationens state har förändrats över en viss tid.

Redux skapades av Dan Abramhov och Andrew Clark år 2015, inspirerat av det funktionella programmeringsspråket Elm samt Facebook’s Flux-arkitektur. Användandet av Redux är inte beroende av att du applicerar det på en applikation byggt i React. Redux är så kallat Framework agnostic och du kan likväl använda ramverk såsom Angular eller Vue JS i samband med Redux.

När och varför ska vi använda Redux

Att införa Redux i en applikation är inte alltid en självklarhet och i många mindre applikationer kanske det inte behövs ett bibliotek för att hantera states. Applikationer du skapat kommer troligtvis göra detsamma oavsett om du använder dig av Redux eller inte. Även grundaren Abamov varnar i ett inlägg om att det finns en viss risk med att föra in Redux i ett för tidigt stadie - delvis eftersom Redux kommer att föra med sig ett ytterligare abstraktionsskikt till din applikation. Därmed rekommenderar jag dig att verkligen ta dig tiden att förstå de faktiska fördelarna med Redux och ta ett beslut därefter. I slutändan, när bitarna faller på plats, medför Redux trots allt en trygghet och även extra redskap som underlättar utvecklingsarbetet.

För att underlätta och faktiskt förstå när det är läge att applicera Redux är om du;

  1. Behöver ett ”single source of truth’ för ditt state.
  2. Har flertalet states som kommer att förändras över tid.

En annan faktor som spelar in, som i dagsläget gör att jag anser Redux som ett självklart val i de flesta applikationer, är att i och med att vi skriver mer och mer single-page-applikationer som i sin tur är mer komplexa än tidigare, har det i sin tur bidragit till att vår kod måste hantera fler olika states än vad vi behövde förut – allt från svar vi fått från servern till states i vårt användargränssnitt såsom valda tabbar i en meny eller spinners. I och med att våra states ständigt ändras och beroenden mellan vyer ökar hamnar vi lätt i ett läge där vi tappar kontrollen över när och vart i vår applikation som statet ändrades. Här gör Redux en enorm nytta.

 

Tre beståndsdelar

En applikation som använder Redux behöver bestå av tre delar som tillsammans binder ihop hela Redux-cirkeln. Dessa tre är actions, reducers och store

Hela applikationen har ett globalt state som finns sparat som ett objekt i en store. För att kunna ändra sitt globala state skickar man iväg actions, som är ett objekt som beskriver vad som händer. Slutligen, för att faktiskt utlösa en förändring i vårat state skriver vi ”pure functions”, i form av reducerers, som specificerar hur våra actions ändrar det globala statet.

Nedan visas ett enkelt exempel, i form av en räknare, där vi i ett objekt specificerar vilken mutation vi vill ska ske med hjälp av actions istället för att förändra vårt globala state direkt. Sedan har vi en ”pure function” som, beroende på vilken action som vi valt att dispatcha (läs skicka iväg) uppdaterar hela applikationens state som lagras i vår store.

import { createStore } from 'redux'

/** Reducer (pure function) */
function
 counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}

/** Redux store */
let store = createStore(counter)

store.subscribe(() => console.log(store.getState()))

/** Dispatch actions */
store.dispatch({ type: 'INCREMENT' })
//1
store.dispatch({ type: 'INCREMENT' })
//2
store.dispatch({ type: 'DECREMENT' })
//1

För att tydligare förstå vad som händer i exemplet ovan går vi igenom vad en store, reducer och action är, var för sig och vad deras respektive syfte är.

Store

Allt som förändras i vår applikation, kopplat till Redux, finns lagrat i vår store. Det globala statet finns här. Det kan exempelvis vara den data vi hämtat från något API som vi sedan vill presentera på olika ställen i applikationen. Det enda sättet för att uppdatera det state som finns i vår store är att gå via en reducer.

Reducers

En reducer är en JavaScript-funktion som tar två inputparametrar, nuvarande statet och en action. För att uppdatera vårt globala state behöver vi som sagt hantera en händelse i vår reducer. Reducerns funktion är således att ändra hur vårt state såg ut i vår store innan och returnera ett nytt state som applikationen nu ska anpassa sig efter. Reducern talar alltså om hur applikationens state ändras och att rätt del blir uppdaterat. Det som beskriver vad som händer är en action.

Actions

Actions är JavaScript-objekt. De innehåller information som skickas från vår applikation till vår store överlag innehåller den två delar. Den första delen, som en Action måste ha, är attributet type som pekar på vilken typ av händelse som har inträffat och dessa definieras oftast som en konstant av strängar. Den andra attributet som en action innehåller, men inte måste, är payload. En payload innehåller ytterligare information om något som ska skickas med i en actions. För att skicka iväg en action och för att således uppdatera sitt globala state i ens store använder man sig av store.dispatch().

Nedan är ett konkret exempel på hur en action kan se ut:

const Action(participantList) {
return {
type: "SUCCESS_FETCHING_DATA",
payload: participantList
}
};

 


Huvudprinciper inom Redux

Single source of truth

Beroende på vilken utvecklingsbakgrund du kommer ifrån har du säkerligen en viss uppfattning av vad "single source of truth" betyder. Eftersom Redux kretsar just kring det här begreppet är det viktigt att ha en god förståelse kring vad som menas med det och vad vi menar när vi nämner "single source of truth" är att statet för hela din applikation finns sparat i ett objekt som återfinnas i en och endast en store. Detta innebär alltså att det enda sättet att ändra data i ett användargränssnitt (om vi nu pratar om React-Redux applikationer där Redux är vårt 'single source of truth') är att dispatcha en Redux action som i sin tur kommer ändra statet i en reducer. Våra komponenter i React kommer då att titta på vår reducer, och om reducern uppdaterar vårt state kommer således vårt användargränssnitt att ändras därefter - men aldrig åt det andra hållet eftersom vårt Redux state är 'single source of truth'.

State is read-only

Att ett state klassas som "read only" innebär att det enda sättet att ändra det är att utföra en action (ett objekt som beskriver vad som har hänt). Orsaken till att man vill göra på det här sättet är för att försäkra sig om att varken våra vyer eller callbacks från anrop ska ha möjlighet att omedelbart påverka vårt state, utan att det istället skickar en förfrågan om att omvandla vårt state och då alla ändringar sker en efter en i en strikt ordning kommer vi inte behöva hantera jobbiga race conditions.

Changes are made with pure functions

Den sista principen som är nödvändig att känna till handlar om att huruvida vi vill ändra statet, om det är att lägga till ett objekt i en lista eller ändra namn på en titel i en bok, är att vi skriver reducers (i form av pure functions) som uppdaterar vårt globala state med hjälp av actions. För att uppnå detta tar vi vårt föregående state och en action, som returnerar nästa state. Antalet reducers ökar oftast i takt med att applikationen blir större och mer komplex. Det som från början räcker att hantera i en enskild reducer blir snabbt uppdelat i flera mindre reducers som hanterar endast den delen av vårt globala state som är nödvändigt. 

Så vad är egentligen en pure function och varför är det användbart när vi jobbar med Redux? Pure functions är stommen i funktionell programmering och är en funktion som behöver uppfylla två strikta kriterier för att kunna klassas som just en pure function:

  1. Returvärdet från funktionen är alltid detsamma för samma argument.
  2. Det finns inga sidoeffekter (inga lokala variabler eller någon form av mutation av lokala variabler etc.).

Nedan följer ett exempel som tydligt belyser skillnaden mellan en pure function och en impure function och hur dess returvärde får olika värden beroende på uppbyggnaden av funktionen.

Samma input ger samma output

const addNumber = (x, y) => x + y;

addNumber(1, 2);

Följande funktion kommer returnera värdet 3.

Jämför det med följande funktion:

let x = 1

const addNumber = (y) => { x += y;};
addNumber(2)
 

Även denna kommer i detta fall att returnera (men endast första gången!). Skillnaden är att i det första exemplet kommer vi alltid att returnera samma värde oavsett var och när vi kallar på den. I det andra exemplet har vi ett delat state som inte omfattas av funktionen vilket kommer leda till att vi får olika resultat beroende på när vi kallar på funktionen. I det här fallet kommer vi först returnera värdet 3, nästa gång kommer den returnera 5 osv. Detta är alltså inte en pure function.

Fördelen med pure function är många, däribland att de är lätta att förstå, lätta att kombinera med varandra men även det som gör att de är optimala att använda i kontexten med Redux - de är lätta att testa! Att skriva enhetstester för våra reducers blir därmed väldigt enkelt just eftersom vi utgår från en funktion som inte tillför sidoeffekter, givet samma input kommer vi alltid att returnera samma output samt att vi inte förlitar oss på utomstående states. Och just eftersom det är i våra reducers som mycket av affärslogiken ligger samt applikationens nästkommande states skapas baserat på APIer eller andra interna svar är det till vår fördel att den här delen är lätt att testa.


Sådär! Nu har vi gått igenom grunderna för Redux, vad det är och varför vi vill använda det. Längre fram tänker jag ta vid där vi står nu med Redux och förklara hur vi kan sammanfoga det med React.