Skip to content

Instantly share code, notes, and snippets.

@markusra
Last active April 6, 2020 18:02
Show Gist options
  • Save markusra/087413af9f255a6847b7c22537967dbc to your computer and use it in GitHub Desktop.
Save markusra/087413af9f255a6847b7c22537967dbc to your computer and use it in GitHub Desktop.

Intro til React Hooks – en workshop

Om workshopen

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.

Lokal utvikling

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.

Løsningsforslag

Står du fast? Spør oss om hjelp eller se på løsningsforslaget til hver oppgave. Du finner disse i solutions-mappen.

Oppgaver

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.


Oppgave 0️⃣: Konfigurer chatten

  1. Gå til sandboxen: https://codesandbox.io/s/5452lx6qo4

  2. Åpne filenconfig.js og endre name i konstanten config til et selvvalgt brukernavn.

  3. Bytt også ut imgId med et vilkårlig tall mellom 1 og 1000.


Oppgave 1️⃣: Sette status

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.

Kort om useState

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).

Oppgavebeskrivelse

I denne oppgaven ønsker vi å kunne endre status ved å trykke på profilbildet vårt.

Instruksjoner

  1. I Profile-komponenten, erstatt status-konstanten med en useState hook. Kall tilstanden status. Initialverdien skal være "online".

    ℹ️Tips: Husk å importere useState fra React.

  2. Utvid toogleStatus til å kalle oppdateringsfunksjonen du får fra useState med verdien newStatus.

    ℹ️Tips: Husk å legge til onClick<img>-taggen


Oppgave 2️⃣: Hente og oppdatere status

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.

Kort om useEffect

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>
}

Oppgavebeskrivelse

For denne oppgaven ønsker vi at du setter statusen ved bruk av localStorage

Instruksjoner

  1. Endre initialverdien i useState til å hente verdien fra localStorage hvis den eksisterer, eller “online” hvis den ikke eksisterer.

    ℹ️Tips: Bruk window.localStorage.getItem(key)

  2. Bruk useEffect til å sette statusen i localStorage.

    ℹ️Tips: Bruk window.localStorage.setItem(key, value)


Oppgave 3️⃣: Optimalisering av useState og useEffect

I denne oppgaven skal vi se på noen grep vi kan ta for å optimalisere useState og useEffect.

Optimalisering av useState

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.

Optimalisering av useEffect

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 componentDidMounti 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.

Oppgavebeskrivelse

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.

Instruksjoner

  1. Endre initialverdien i useState til å bruke () => some_value.

  2. Endre useEffect slik at effekten kun trigges når statusen endrer seg.


Oppgave 4️⃣a: Lage en Custom Hook (del 1)

I denne oppgaven skal vi kombinere ulike hooks til en kombinert hook. Dette kaller vi for en Custom Hook.

Kort om Custom Hooks

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;
}

Oppgavebeskrivelse

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.

Instruksjoner

  1. Lag en custom hook som du kan kalle useLocalStorageState som bruker useState og useEffect.

  2. For at denne Custom Hooken blir så gjenbrukbar som mulig, må både localStorage-nøkkelen og initialverdien sendes med som argumenter.


Oppgave 4️⃣b: Lag en Custom Hook (del 2)

Vi jobber fortsatt i Profile-komponenten, og skal nå lage enda en Custom Hook.

Oppgavebeskrivelse

I denne oppgaven ønsker vi å lage en egen hook som gjenbruker hooken fra oppgave a) som håndterer localStorageStaten for statusen vår.

Instruksjoner

  1. 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>
  );
}

Oppgave 5️⃣: Motta meldinger

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.

Oppgavebeskrivelse

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.

Instruksjoner:

  1. Lag en ny fil, useMessages, i utils-mappen.

  2. Lag en Custom Hook som returnerer meldingene i applikasjonen. ChatAPI.receiveLastTenMessages() kan brukes til å hente de ti siste meldingene, og ChatAPI.receiveMessage() til å hente nye meldinger fortløpende.

  3. Ta i bruk denne hooken i Messages-komponenten.

  4. <Message /> tar inn to props: key og data. 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
    }}
    

Oppgave 6️⃣: Sende meldinger

I denne oppgaven skal vi endelig sende meldinger! Til dette skal vi bruke useReducer.

Kort om 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å.

Oppgavebeskrivelse

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 statesmessage 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.

Instruksjoner

  1. 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 til message vil vi også ha tilstand for characterCount for å holde styr på antall tegn.
  2. Lag en reducer-funksjon som har to actions:
    • SET_MESSAGE: Returnerer et objekt med den oppdaterte tilstanden til message og characterCount

      ℹ️Tips: Begge bruker action.message

    • RESET_MESSAGE: Returnerer et objekt som resetter message til en tom streng ("") og characterCount til 0.
  3. Dispatch SET_MESSAGE med meldingen som verdi i onMessageChanged
  4. I sendMessage vil vi kalle ChatAPI.sendMessage-funksjonen med imgId og name fra config, og message fra tilstanden. Deretter vil vi dispatche RESET_MESSAGE.
  5. Oppdater verdien til <input /> og characterCount-<span> til å rendre riktige tilstander.

Oppgave 7️⃣: Se hvem som skriver

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.

Oppgavebeskrivelse

Opprett funksjonalitet for å kunne se hvem som skriver i chat-applikasjonen

Instruksjoner

  1. Legg til en action SET_TYPING_USERS i reduceren som returnerer hvem som skriver.

ℹ️Tips: action.typingUsers

  1. Siden vi nå ikke returnerer alle tilstandsvariabler i alle actions må vi også sende med de uforandrede variablene. Dette kan gjøres med ...state.
  2. MessageField trenger en hook som kaller ChatAPI.setIsTyping og ChatAPI.receiveTypingUsers. Sistnevnte skal dispatche SET_TYPING_USERS med typingUsers-verdien.

ℹ️Tips: Hooken avhenger av en variabel.

  1. I onMessageChanged må vi også dispatche RESET_MESSAGE hvis meldingsfeltet er tomt.

Oppgave 8️⃣: Sette en personlig statustekst

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.

Kort om Context APIet

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>;
}

Oppgavebeskrivelse

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.

Instruksjoner

  1. Lag en ny fil, AppContext, som ikke gjør annet enn å eksportere en ny kontekst (bruk React.createContext()).

  2. Opprett enda en fil, AppProvider, som lar oss "wrappe" konkeksten rundt en komponent.

    Tips: Bruk props.children.

  3. 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.

  4. Utvid AppProvider til å bruke useLocalStorage (custom hook som vi allerede har definert i localstorage-util).

  5. Bruk useContext i MessageField-komponenten til å sette statusteksten.

  6. Bruk useContext i Profile-komponenten til å hente verdien.

  7. Vis verdien når du beveger musepekeren over profilbildet ditt (send verdien med tooltipText-propen til Tooltip-komponenten).

  8. Gjør det samme for kontaktlisten.


Oppgave 9️⃣: Sette fokus på inputfeltet

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.

Oppgavebeskrivelse

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.

Instruksjoner

  1. Bruk useRef i AppProvider til å definere et nytt ref-objekt.
  2. Sett ref-objektet som enda en verdi i AppProvider (dvs. i tillegg til statusteksten du definerte i forrige oppgave).
  3. I MessageField setter du ref-propen til ref-objektet i den nyopprettede konteksten
  4. I useLocalStorageStatus kan du igjen bruke konteksten til å sette focus på inputfeltet

    ℹ️Tips: Bruk [REF_OBJEKT].current.focus()


Bonusoppgaver

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.

Endre brukernavn og profilbilde

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.

Vise lenker i meldinger

Ut av boksen fungerer det ikke å legge inn lenker i chatterommet. Utvid applikajsonen til å vise javazone.no som en lenke man kan trykke på.

Vise bilder i meldinger

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.

Filtrere kontaktlisten basert på status

Av og til kan det være ønskelig å kun se brukere som er "online" og tilgjengelige i kontaklisten. Legg til funskjonalitet for dette.

Filtrere meldinger på søkeord

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.


Nyttige lenker

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment