I denne workshopen har vi satt opp et enkelt frontendskall for en chatteapplikasjon. Selve applikasjonen lever på CodeSandbox og kan nåes på denne lenken: https://codesandbox.io/s/5452lx6qo4.
Er du ikke fan av CodeSandbox? Da kan du laste ned prosjektet som zip-fil, kjøre npm install
og npm start
, og utvikle i din favoritteditor! Trykk "File" og "Export as ZIP", ekspander filen og naviger til mappen.
For å kjøre npm
trenger du å ha node
installert. Det installerer du fra hjemmesiden deres.
Du kan bruke hvilken editor du vil. Vi anbefaler Visual Studio Code, men Sublime Text og WebStorm er også fantastiske alternativer.
Står du fast? Spør oss om hjelp eller se på løsningsforslaget til hver oppgave. Du finner disse i solutions-mappen.
Applikasjonen består av en sidebar som skal vise egen profil (inkl. status) og en liste over alle brukere som er tilkoblet chatteserveren. I tillegg inneholder den selve chatten med alle meldinger som sendes og et tekstfelt som man kan skrive nye meldinger i.
For å spare litt tid er det på forhånd satt opp et React-skjelett og medfølgende styling, i tillegg til et API som kan brukes til å utføre ulike handlinger mot chatteserveren.
-
Gå til sandboxen: https://codesandbox.io/s/5452lx6qo4
-
Åpne filen
config.js
og endrename
i konstantenconfig
til et selvvalgt brukernavn. -
Bytt også ut
imgId
med et vilkårlig tall mellom 1 og 1000.
I den første oppgaven skal vi kun fokusere på selve statusen (den fargede ringen rundt profilbildet ditt) som viser om du er tilkoblet(grønn), borte(gul) eller frakoblet(rød). For å få til dette, kan vi bruke useState hooken.
useState returnerer et array med med verdier – den nåværende tilstanden og en oppdateringsfunksjon.
En useState Hook kan for eksempel se slik ut:
function Counter {
const [count, setCount] = useState(0);
}
Skal du bruke tilstanden, så holder det å skrive count
, i stedet for å skrive this.state.count
slik man er vandt med fra tidligere. For å sette tilstanden skriver du f.eks. setCount(count + 1)
.
I denne oppgaven ønsker vi å kunne endre status ved å trykke på profilbildet vårt.
-
I Profile-komponenten, erstatt status-konstanten med en useState hook. Kall tilstanden
status
. Initialverdien skal være"online"
.ℹ️Tips: Husk å importere useState fra React.
-
Utvid
toogleStatus
til å kalle oppdateringsfunksjonen du får frauseState
med verdiennewStatus
.ℹ️Tips: Husk å legge til
onClick
på<img>
-taggen
I denne oppgaven skal vi videreutvikle det vi gjorde i den første oppgaven, og hente og oppdatere statusen fra localStorage når statusen blir endret.
LocalStorage er en form for lagring av informasjon som websider og applikasjoner kan benytte seg av, uten en utløpsdato. Dette betyr at data lagret i nettleseren vil overleve, selvom du lukker nettleseren.
LocalStorage tillater å gjøre enkle operasjoner som (lagre, lese, oppdatere og slette). Denne siden går mer i dybden på de forskjellige metodene som kan benyttes i localStorage.
En sideeffekt er noe som påvirker noe utenfor React-verdenen. Det kan være å kalle DOM-APIer, hente data eller noe helt annet. useEffect tar i mot en funksjon som skal utføre sideeffektene for oss. Den tar i mot en callback-funksjon som React vil kalle etter at DOMen har blitt oppdatert.
useEffect(() => {
// Her kan du gjøre HTTP requests, kall på nettleserens API, endre på DOMen osv.
})
Hvis vi fortsetter med count-eksempelet fra forrige oppgave, kan dette se ut som følgende med useEffect:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return <div>{count}</div>
}
For denne oppgaven ønsker vi at du setter statusen ved bruk av localStorage
-
Endre initialverdien i
useState
til å hente verdien fra localStorage hvis den eksisterer, eller“online”
hvis den ikke eksisterer.ℹ️Tips: Bruk
window.localStorage.getItem(key)
-
Bruk
useEffect
til å sette statusen i localStorage.ℹ️Tips: Bruk
window.localStorage.setItem(key, value)
I denne oppgaven skal vi se på noen grep vi kan ta for å optimalisere useState og useEffect.
Vi trenger faktisk kun å vite verdien i localStorage første gang komponenten rendres. Det vil si at alle de andre kallene er bortkastet.
For å komme oss unna dette problemet, kan useState ta inn en funksjon i stedet for selve verdien. Dette sørger for at verdien kun hentes første gang komponenten blir rendret.
Det vil si at vi kan gå fra å gjøre dette:
useState(someExpensiveFunction())
Til dette:
useState(() => someExpensiveFunction())
Nå vil bare someExpensiveFunction()
bli kalt når det er bruk for den.
I applikasjonen vår ønsker vi at localStorage kun skal bli oppdatert når status
endrer seg – den trenger ikke å endres på hver render (slik som den gjør nå, uten et 2. argument). useEffect
tillater å sende med et argument til, noe React kaller dependency array.
useEffect(()=> {
Do something every render
})
useEffect(()=>{
Do something only when some_argument changes
}, [some_argument])
Dette arrayet definerer når effekten skal trigges. Et tomt array indikerer at effekten ikke er avhengig av noen state-variabler eller props, så den vil aldri trenge å bli kjørt en gang til, tilsvarende componentDidMount
i klassekomponenter. Legger man derimot til en eller flere state-variabler eller props i dette arrayet vil effekten trigges hver gang denne/disse statene eller propsene endres.
Slik som applikasjonen er satt opp nå, leser useState fra localStorage hver gang komponenten kjører og useEffect trigges på hver render. Dette kan være tregt og kan skape flaskehalser. Vi vil derfor oppdatere useState og useEffect funksjonene våre.
-
Endre initialverdien i useState til å bruke
() => some_value
. -
Endre useEffect slik at effekten kun trigges når statusen endrer seg.
I denne oppgaven skal vi kombinere ulike hooks til en kombinert hook. Dette kaller vi for en Custom Hook.
Når vi har lyst til å dele logikken mellom to JavaScript-funksjoner, abstraherer vi den ut i en tredje funksjon. Når både komponentene og hookene er funksjoner fungerer dette fint for dem også.
Etterhvert som vi begynner å ta i bruk hooks, ser vi at flere og ulike hooks brukes til å oppnå et felles mål. F.eks. bruker vi ofte en variabel opprettet ved hjelp av useState i useEffect. Disse hooksene kan samles i det som kalles Custom Hook, og på denne måten gjenbrukes flere steder.
En Custom Hook er i bunn og grunn ikke noe annet enn en funksjonskomponent med navn som starter på use
. Dette er først og fremst for at det skal være enkelt å forstå hva den blir brukt til.
Et typisk eksempel på en Custom Hook kan se slik ut:
function useCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return count;
}
I denne oppgaven skal vi refaktorere Profile-komponenten til å bruke en Custom Hook som tar seg av håndteringen av useState og useEffect i stedet for komponenten selv.
-
Lag en custom hook som du kan kalle
useLocalStorageState
som bruker useState og useEffect. -
For at denne Custom Hooken blir så gjenbrukbar som mulig, må både localStorage-nøkkelen og initialverdien sendes med som argumenter.
Vi jobber fortsatt i Profile-komponenten, og skal nå lage enda en Custom Hook.
I denne oppgaven ønsker vi å lage en egen hook som gjenbruker hooken fra oppgave a) som håndterer localStorageStaten for statusen vår.
- Utenfor profile-komponenten lag en egen hook
useLocalStorageStatus
som tar seg av det meste av funktionaliteten til Profile-komponenten vår.
Til slutt skal Profile-komponenten se cirka slik ut:
export default function Profile() {
const [status, toggleStatus] = useLocalStorageStatus();
return (
<div id="profile">
...
</div>
);
}
I denne oppgaven skal vi utvide applikasjoen til å kunne motta meldinger som blir sendt av andre brukere. For å gjøre dette bruker vi useEffect til å hente data fra chat-APIet.
Vi skal hente de ti siste meldingen når brukeren kobler til chatten, og legge til rette for at vi kan motta noen meldinger fortløpende.
-
Lag en ny fil, useMessages, i utils-mappen.
-
Lag en Custom Hook som returnerer meldingene i applikasjonen.
ChatAPI.receiveLastTenMessages()
kan brukes til å hente de ti siste meldingene, ogChatAPI.receiveMessage()
til å hente nye meldinger fortløpende. -
Ta i bruk denne hooken i Messages-komponenten.
-
<Message />
tar inn to props:key
ogdata
.Key
brukeruuidv1
som tildeler hver melding en unik key (React krever at hvert element i en liste har en unik key-prop).data
-propen må være struktert som følgende:data={{ imgId: message.imgId, sender: message.name, message: message.message, timestamp: message.timestamp }}
I denne oppgaven skal vi endelig sende meldinger! Til dette skal vi bruke useReducer.
useReducer er et alternaltiv til useState som returnerer et array med to elementer i likhet med useState. Det første elementet er current state
og det andre elementet er dispatch
-funksjonen. Dersom Redux er kjent, vet du hvordan dette fungerer.
useReducer er vanligvis foretrukket fremfor useState når vi har kompleks state-logikk med mange state-variabler eller når den neste staten avhenger av den forrige.
::: info ℹ️ Tips useReducer Hooken lar oss også optimalisere komponeneter som har oppdateringer som skjer lenger ned i applikasjonen ved å sende dispatch ned i komponenten i stedet for å bruke callbacks. :::
Vi kan se på et count-eksempel
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return {count: state.count + 1};
case 'DECREMENT':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter({initialState}) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'INCREMENT'})}>+</button>
<button onClick={() => dispatch({type: 'DECREMENT'})}>-</button>
</>
);
}
Her har vi to forskjellige tilstander som enten øker count
med 1 eller minker count
med 1 ettersom hvilken knapp du trykker på.
I tillegg til å sende meldinger vil vi også samtidig holde styr på antall tegn som blir skrevet. Det vil si at vi vil ha to states – message
og characterCount
– i tillegg til å kalle sendMessage
-funksjonen til APIet. Siden vi også skal legge til flere states i neste oppgave passer det bra å bruke useReducer her for å slippe å holde styr på mange (use)States.
- I MessageField-komponenten finnes det en konstant kalt
message
. Vi vil erstatte denne med en tilstand, og til det skal vi bruke useReducer. I tillegg tilmessage
vil vi også ha tilstand forcharacterCount
for å holde styr på antall tegn. - Lag en reducer-funksjon som har to actions:
SET_MESSAGE
: Returnerer et objekt med den oppdaterte tilstanden tilmessage
ogcharacterCount
ℹ️Tips: Begge bruker
action.message
RESET_MESSAGE
: Returnerer et objekt som resettermessage
til en tom streng (""
) ogcharacterCount
til 0.
- Dispatch
SET_MESSAGE
med meldingen som verdi ionMessageChanged
- I
sendMessage
vil vi kalleChatAPI.sendMessage
-funksjonen medimgId
ogname
fraconfig
, ogmessage
fra tilstanden. Deretter vil vi dispatcheRESET_MESSAGE
. - Oppdater verdien til
<input />
og characterCount-<span>
til å rendre riktige tilstander.
I en chat er det fint å kunne se hvilke brukere som holder på å skrive noe. Vi skal utvide MessageField-komponenten til å støtte nettopp dette.
Opprett funksjonalitet for å kunne se hvem som skriver i chat-applikasjonen
- Legg til en action
SET_TYPING_USERS
i reduceren som returnerer hvem som skriver.
ℹ️Tips:
action.typingUsers
- Siden vi nå ikke returnerer alle tilstandsvariabler i alle actions må vi også sende med de uforandrede variablene. Dette kan gjøres med
...state
. - MessageField trenger en hook som kaller
ChatAPI.setIsTyping
ogChatAPI.receiveTypingUsers
. Sistnevnte skal dispatcheSET_TYPING_USERS
medtypingUsers
-verdien.
ℹ️Tips: Hooken avhenger av en variabel.
- I
onMessageChanged
må vi også dispatcheRESET_MESSAGE
hvis meldingsfeltet er tomt.
Hadde det ikke vært kult å kunne satt en statustekst som alle andre i chatterommet kan se? Vi skal bruke Context APIet til React for å få til dette.
Forestill deg at du har en React-app som har et komplekst komponenttre med mange nivåer. Når man skal sende data nedover i treet, så bruker man som regel props. Vi risikerer da at disse props sendes via mange komponenter som selv ikke bruker propsene for å nå målet. Dette kalles for prop drilling.
Men det finnes heldigvis en måte vi kan unngå dette på, nemlig Reacts Context API. APIet gjør det mulig å få tilgang til data på forskjellige nivåer uten å måtte sende det gjennom props. Her lages det på en måte snarveier som komponentene våre kan benytte seg av. Har du ikke brukt Context før, så kan du ta en titt på dokumentasjonen.
Context APIet til React har eksistert en god stund. Det som er nytt er at det er lagt til en egen Hook for å aksessere konteksten, useContext. useContext
gir den samme funksjonaliteten du forventer fra React sitt Context API, bare at den er pakket inn i Hooks som kan benyttes i funksjonelle komponenter.
Komponenten som oppretter og tilgjengeliggjør konteksten kan se slik ut:
function AppProvider() {
const AppContext = React.createContext();
return (
<AppContext.Provider value="someValue">
<MyChildComponent />
<MyOtherChildComponent />
</context.Provider>
)
}
Komponenten som skal bruke ("konsumere") konteksten ser slik ut:
function MyChildComponent() {
const appContext = useContext(AppContext)
return <div>{appContext}</div>;
}
Statusteksten skal settes når du skriver /status STATUS_TEKST
i tekstfeltet og trykker ENTER. Beveger du musepekeren over ditt eget profilbilde eller andres profilbilde i sidebaren, så skal det dukke opp en tooltip som inneholder statusteksten til vedkommende. Vi skal også lagre statusteksten i LocalStorage (som vi brukte i Oppgave 4b), slik at teksten fortsatt er der selv om vi refresher siden.
-
Lag en ny fil, AppContext, som ikke gjør annet enn å eksportere en ny kontekst (bruk
React.createContext()
). -
Opprett enda en fil, AppProvider, som lar oss "wrappe" konkeksten rundt en komponent.
Tips: Bruk
props.children
. -
Legg AppProvider rundt hele AppContainer-komponenten i index.js. På den måten får vi tilgang til konteksten fra alle komponenter i appen vår.
-
Utvid AppProvider til å bruke
useLocalStorage
(custom hook som vi allerede har definert ilocalstorage-util
). -
Bruk
useContext
i MessageField-komponenten til å sette statusteksten. -
Bruk
useContext
i Profile-komponenten til å hente verdien. -
Vis verdien når du beveger musepekeren over profilbildet ditt (send verdien med
tooltipText
-propen til Tooltip-komponenten). -
Gjør det samme for kontaktlisten.
Når vi endrer på statusen vår forsvinner fokuset fra inputFeltet vårt. Vi ønsker at inputfeltet automatisk skal få fokus tilbake etter at statusen er endret. Dette kan vi løse med å bruke useRef. useRef
oppdaterer komponenten uten at den re-rendres. Hvis du er kjent med createRef
fra klasse-komponenter, fungerer useRef
på samme måte.
I denne oppgaven ønsker vi igjen å kunne referere til MessageField fra Profile-komponenten, så da kan vi fortsette å bruke AppContexten
vår. Vi vil utvide AppProvider
til å holde på en global inputreferanse, som kan oppdatere MessageField
når vi endrer en state i Profile
.
- Bruk
useRef
i AppProvider til å definere et nytt ref-objekt. - Sett ref-objektet som enda en verdi i AppProvider (dvs. i tillegg til statusteksten du definerte i forrige oppgave).
- I MessageField setter du
ref
-propen til ref-objektet i den nyopprettede konteksten - I
useLocalStorageStatus
kan du igjen bruke konteksten til å sette focus på inputfeltetℹ️Tips: Bruk
[REF_OBJEKT].current.focus()
Hvis du skulle bli ferdig før tiden eller bare ikke klarer å legge fra deg denne applikasjonen, så kan du fortsette med å legge til én eller flere av disse funksjonalitetene. For disse oppgavene finnes det ingen løsnisngsforslag, så her står du litt mer fritt til å gjøre det som du vil.
I stedet for å måtte endre brukernavn og profilbildet i config-filen hadde det vært kult om dette kunne gjøres direkte i chatteapplikasjonen. Her er det kun fantasien som setter grenser.
Ut av boksen fungerer det ikke å legge inn lenker i chatterommet. Utvid applikajsonen til å vise javazone.no
som en lenke man kan trykke på.
Enda kulere enn å kunne vise lenker er det om vi kan vise bilder også. I denne oppgaven ønsker vi at du skal utvide appliksjonen til å kunne gjøre akkurat dette.
Av og til kan det være ønskelig å kun se brukere som er "online" og tilgjengelige i kontaklisten. Legg til funskjonalitet for dette.
Når chatten har vokst seg stor og det ligger mange meldinger der, er det en fordel om vi kan søke på tekst for å finne igjen det som er skrevet tidligere. Legg til funksjonalitet for å søke etter fritekst og vis kun de meldingene som treffer søket.
- Offisiell Hooks dokumentasjon: https://reactjs.org/docs/hooks-intro.html
- Hooks FAQ: https://reactjs.org/docs/hooks-faq.html
- Dan Abramov om Hooks: https://dev.to/dan_abramov/making-sense-of-react-hooks-2eib
- Bloggen til Dan Abramov: https://overreacted.io/