├── public ├── favicon.ico ├── manifest.json └── index.html ├── src ├── App.js ├── sett-brukernavnet-ditt-her.js ├── index.js ├── data │ └── images.js ├── server.js └── index.css ├── .gitignore ├── package.json └── README.md /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bekk/react-intro/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function App() { 4 | return null; 5 | } 6 | 7 | export default App; 8 | -------------------------------------------------------------------------------- /src/sett-brukernavnet-ditt-her.js: -------------------------------------------------------------------------------- 1 | // Her må du spesifisere brukernavnet ditt! 2 | // Det vil dukke opp på alle bilder, kommentarer og likes 3 | const brukernavn = 'Prison Mike'; 4 | 5 | export default brukernavn; 6 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Bekkstagram", 3 | "name": "Bekkstagram", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#fff", 14 | "background_color": "#1b1b1b" 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import '@reach/dialog/styles.css'; 4 | import './index.css'; 5 | import App from './App'; 6 | 7 | // ⚠️ OBS ⚠️ 8 | // Du trenger ikke tenkte så mye over hva som ligger her 9 | // Det er for det meste oppsett for alle oppgavene i hele 10 | // workshopen, så det kan se litt mye ut - men det er mest fordi 11 | // det er en workshop. 12 | // 13 | // Gå til App.js og start med oppgave 1! 14 | 15 | ReactDOM.render(, document.getElementById('root')); 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-intro", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@reach/dialog": "^0.2.9", 7 | "date-fns": "^2.0.1", 8 | "react": "^16.9.0", 9 | "react-dom": "^16.9.0", 10 | "react-icons": "^3.7.0", 11 | "react-router-dom": "^5.1.2", 12 | "react-scripts": "3.1.1" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start" 16 | }, 17 | "eslintConfig": { 18 | "extends": "react-app" 19 | }, 20 | "browserslist": { 21 | "production": [ 22 | ">0.2%", 23 | "not dead", 24 | "not op_mini all" 25 | ], 26 | "development": [ 27 | "last 1 chrome version", 28 | "last 1 firefox version", 29 | "last 1 safari version" 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/data/images.js: -------------------------------------------------------------------------------- 1 | const DAY = 1000 * 60 * 60 * 24; 2 | // Dette er arrayet du skal liste ut 3 | // Det inneholder bilder på formatet 4 | // { id: 1, url: 'http://url.com', description: 'alt-tekst' } 5 | const images = [ 6 | { 7 | id: '1', 8 | url: 9 | 'https://images.unsplash.com/photo-1556564582-374df0d7577c?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=800&q=60', 10 | description: 'En person som sparker', 11 | createdDate: new Date() - DAY * 2, 12 | username: '@olav', 13 | }, 14 | { 15 | id: '2', 16 | url: 17 | 'https://images.unsplash.com/photo-1556575157-15758d4d0d19?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=800&q=60', 18 | description: 19 | 'Tre steinmunker som holder seg for henholdsvis ører, munn og øyne', 20 | createdDate: new Date() - DAY * 3, 21 | username: '@lillesand', 22 | }, 23 | { 24 | id: '3', 25 | url: 26 | 'https://images.unsplash.com/photo-1556595015-dda91ab57740?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=800&q=60', 27 | description: 'En foss i Yosemite nasjonalpark i USA', 28 | createdDate: new Date() - DAY * 4, 29 | username: '@bendik_iversen', 30 | }, 31 | { 32 | id: '4', 33 | url: 34 | 'https://images.unsplash.com/photo-1556609516-87806077156a?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=800&q=60', 35 | description: 'To asiatiske ungdommer som står på en stor saltslette', 36 | createdDate: new Date() - DAY * 12, 37 | username: '@selbekk', 38 | }, 39 | { 40 | id: '5', 41 | url: 42 | 'https://images.unsplash.com/photo-1556560024-b4e093c161a3?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=800&q=60', 43 | description: 'Bagasjerommet til en klassisk Volvo', 44 | createdDate: new Date() - DAY * 50, 45 | username: '@marie', 46 | }, 47 | ]; 48 | 49 | export default images; 50 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import username from './sett-brukernavnet-ditt-her'; 2 | 3 | // ⚠️ OBS ⚠️ 4 | // Denne filen inneholder kode som kaller serveren vår 5 | // Du skal ikke egentlig trenge å forholde deg til denne koden, 6 | // men om du debugger litt, eller lurer på hvorfor noe ikke fungerer, 7 | // er det alltids lov å ta en titt. 8 | 9 | const API_BASE_URL = 'https://bekkstagram-api.herokuapp.com/api'; 10 | 11 | export const getFeed = async () => { 12 | const response = await fetch(`${API_BASE_URL}/media`); 13 | const { data } = await response.json(); 14 | return data; 15 | }; 16 | 17 | export const getImage = async id => { 18 | const response = await fetch(`${API_BASE_URL}/media/${id}`); 19 | const { data } = await response.json(); 20 | return data; 21 | }; 22 | 23 | const validateImage = url => 24 | new Promise(resolve => { 25 | const image = new Image(); 26 | image.onload = () => resolve(true); 27 | image.onerror = () => resolve(false); 28 | image.src = url; 29 | }); 30 | 31 | export const uploadImage = async ({ url, description }) => { 32 | const isValidUrl = await validateImage(url); 33 | if (!isValidUrl) { 34 | throw new Error("Image URL wasn't valid"); 35 | } 36 | 37 | const response = await fetch(`${API_BASE_URL}/media`, { 38 | method: 'POST', 39 | body: JSON.stringify({ url, description, username }), 40 | headers: { 41 | 'Content-Type': 'application/json', 42 | }, 43 | }); 44 | 45 | const { data } = await response.json(); 46 | return data; 47 | }; 48 | 49 | export const putComment = async (imageId, comment) => { 50 | const commentObject = { text: comment, username }; 51 | 52 | const response = await fetch(`${API_BASE_URL}/media/${imageId}/comments`, { 53 | method: 'PUT', 54 | body: JSON.stringify(commentObject), 55 | headers: { 56 | 'Content-Type': 'application/json', 57 | }, 58 | }); 59 | 60 | const { data } = await response.json(); 61 | return data; 62 | }; 63 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --app-background: white; 3 | --dark-text: #121212; 4 | --light-text: #ebebeb; 5 | 6 | --app-text: var(--dark-text); 7 | 8 | --component-background: #ebebeb; 9 | --component-hover: #e2e2e2; 10 | 11 | --input-text: var(--dark-text); 12 | --input-border: #585858; 13 | --input-placeholder: #9e9e9e; 14 | 15 | --comment-button-enabled: var(--component-background); 16 | --comment-button-hover: var(--component-hover); 17 | 18 | --add-image-icon: var(--dark-text); 19 | } 20 | 21 | html { 22 | box-sizing: border-box; 23 | } 24 | *, 25 | *::before, 26 | *::after { 27 | box-sizing: inherit; 28 | } 29 | 30 | body { 31 | background-color: var(--app-background); 32 | margin: none; 33 | } 34 | 35 | .App { 36 | font-family: sans-serif; 37 | margin: 0 auto; 38 | } 39 | 40 | .site-header { 41 | margin: 0 auto; 42 | width: 200px; 43 | } 44 | 45 | .site-header h1 { 46 | color: var(--app-text); 47 | font-family: Cambria, Cochin, Georgia, Times, 'Times New Roman', serif; 48 | text-align: center; 49 | } 50 | 51 | .site-header h1:hover { 52 | transform: scale(1.1); 53 | } 54 | 55 | .site-header a { 56 | text-decoration: none; 57 | } 58 | 59 | .detail { 60 | max-width: 800px; 61 | margin: 0 auto; 62 | } 63 | 64 | .posts { 65 | max-width: 500px; 66 | margin: 0 auto; 67 | } 68 | 69 | .post { 70 | margin-bottom: 50px; 71 | } 72 | 73 | .post .author { 74 | background-color: var(--component-background); 75 | color: var(--dark-text); 76 | font-weight: bold; 77 | margin: 0; 78 | padding: 10px; 79 | border-radius: 4px 4px 0 0; 80 | } 81 | 82 | .post .image { 83 | display: block; 84 | width: 100%; 85 | margin: 0; 86 | } 87 | 88 | .post .description { 89 | background-color: var(--component-background); 90 | color: var(--dark-text); 91 | margin: 0; 92 | padding: 10px; 93 | border-radius: 0 0 4px 4px; 94 | position: relative; 95 | } 96 | 97 | .post .timestamp { 98 | color: var(--app-text); 99 | margin: 0; 100 | padding: 5px 10px; 101 | text-align: right; 102 | font-size: 80%; 103 | display: inline-block; 104 | } 105 | 106 | .post .likes { 107 | color: var(--app-text); 108 | margin: 0; 109 | padding: 5px 10px; 110 | text-align: left; 111 | font-size: 80%; 112 | display: inline-block; 113 | } 114 | 115 | .post .like-button { 116 | appearance: none; 117 | border: 0; 118 | border-radius: 0; 119 | background: none; 120 | cursor: pointer; 121 | transition: all 0.1s ease-out; 122 | } 123 | 124 | @keyframes thumb { 125 | 0% { 126 | transform: scale(1.25); 127 | } 128 | 25%, 129 | 75% { 130 | transform: scale(1); 131 | } 132 | 50% { 133 | transform: scale(1.5); 134 | } 135 | 100% { 136 | transform: scale(1.25); 137 | } 138 | } 139 | 140 | .post .like-button:focus { 141 | outline: none; 142 | } 143 | 144 | .post .like-button:hover { 145 | animation: thumb 1s ease-in-out infinite; 146 | outline: none; 147 | } 148 | 149 | .post-details { 150 | margin: 0; 151 | padding: 5px; 152 | display: inline-block; 153 | } 154 | 155 | .comments { 156 | margin: 5px 15px 15px 15px; 157 | font-size: 90%; 158 | } 159 | 160 | .comment { 161 | background-color: var(--component-background); 162 | border-radius: 4px 4px 4px 4px; 163 | padding: 10px; 164 | line-height: 25px; 165 | margin-bottom: 15px; 166 | } 167 | 168 | .comment:hover { 169 | background-color: var(--component-hover); 170 | } 171 | 172 | .comment .comment-text { 173 | margin-left: 10px; 174 | } 175 | 176 | .comment .comment-user { 177 | font-weight: bold; 178 | } 179 | 180 | .comment .timestamp { 181 | color: var(--dark-text); 182 | display: block; 183 | margin-left: 0; 184 | padding-top: 5px; 185 | padding-left: 0; 186 | padding-bottom: 0; 187 | text-align: left; 188 | } 189 | 190 | .comment-form { 191 | display: flex; 192 | height: 40px; 193 | } 194 | 195 | .comment-form input { 196 | background-color: var(--app-background); 197 | border: 1px solid var(--input-border); 198 | border-radius: 4px 0px 0px 4px; 199 | color: var(--input-text); 200 | padding: 10px; 201 | width: 100%; 202 | font-size: 90%; 203 | } 204 | 205 | .comment-form input::placeholder { 206 | color: var(--input-placeholder); 207 | } 208 | 209 | .comment-form input:focus { 210 | outline: none; 211 | } 212 | 213 | .comment-form-button { 214 | background-color: var(--comment-button-enabled); 215 | border-top: 1px solid var(--input-border); 216 | border-right: 1px solid var(--input-border); 217 | border-bottom: 1px solid var(--input-border); 218 | border-left: 0; 219 | border-radius: 0px 4px 4px 0px; 220 | color: var(--dark-text); 221 | cursor: pointer; 222 | font-size: 90%; 223 | font-weight: bold; 224 | width: 80px; 225 | } 226 | 227 | .comment-form-button:hover { 228 | background-color: var(--component-hover); 229 | } 230 | 231 | .comment-form-button:focus { 232 | outline: none; 233 | } 234 | 235 | .comment-form-button-disabled { 236 | background-color: var(--app-background); 237 | border-top: 1px solid var(--input-border); 238 | border-right: 1px solid var(--input-border); 239 | border-bottom: 1px solid var(--input-border); 240 | border-left: 0; 241 | border-radius: 0px 4px 4px 0px; 242 | color: var(--light-text); 243 | font-size: 90%; 244 | font-weight: bold; 245 | width: 80px; 246 | } 247 | 248 | .comment-form-button-disabled:focus { 249 | outline: none; 250 | } 251 | 252 | .camera-button { 253 | appearance: none; 254 | background: var(--component-background); 255 | border: none; 256 | border-radius: 50%; 257 | cursor: pointer; 258 | padding: 15px; 259 | position: fixed; 260 | bottom: 20px; 261 | right: 20px; 262 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); 263 | transition: all 0.2s ease-out; 264 | } 265 | 266 | .camera-button:hover { 267 | background: var(--component-hover); 268 | box-shadow: 0 0 10px 5px rgba(0, 0, 0, 0.3); 269 | transform: scale(1.05); 270 | } 271 | 272 | .camera-button:focus { 273 | outline: none; 274 | box-shadow: 0 0 10px 5px rgba(0, 0, 0, 0.3), inset 0 0 10px rgba(0, 0, 0, 0.5); 275 | transform: scale(1.05); 276 | } 277 | 278 | .camera-button > * { 279 | font-size: 2.5em; 280 | color: var(--add-image-icon); 281 | } 282 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduksjon til React – en workshop 2 | 3 | Dette er en workshop for deg som vil lære React fra bunnen av. Workshopen består av et sett med oppgaver, med gode forklaringer etter hver oppgave. 4 | 5 | Når du har jobbet deg gjennom denne workshopen vil du ha en god, grunnleggende forståelse av hvordan React fungerer, og hvordan du kan lage webapper på null komma niks. 6 | 7 | > ## En workshop i _moderne_ React 8 | > 9 | > Denne workshopen fokuserer kun på moderne APIer som funksjonskomponenter og hooks. Vi går ikke igjennom klasser, livssyklus-metoder eller `this`. Du vil mest sannsynlig treffe på disse i eksisterende prosjekter, men som helt ny React-utvikler mener vi at du burde fokusere på måten du kommer til å skrive React på - ikke hvordan det har blitt skrevet. 10 | 11 | 🎉 [Gå til oppgavene](#oppgaver) 🎉 12 | 13 | ## Antatte forkunnskaper 14 | 15 | Vi kommer til å anta at deltakerne i denne workshopen har en grunnleggende forståelse for webutvikling generelt, og moderne JavaScript spesielt. Om du føler at du trenger en oppfriskning i dette, har [Johanne Horn](https://github.com/johhorn) et al skrevet [en fantastisk introduksjonsbok om HTML, CSS og JavaScript](https://johhorn.gitbooks.io/web-intro/). Om du virkelig vil dykke dypt ned, så kan vi også anbefale [JavaScript.info](https://javascript.info/), en fantastisk guide til JavaScript. 16 | 17 | Det er helt okei å ikke kunne alt dette før du begynner. Webutvikling er et stort felt, og selv vi som har laget denne workshopen kan bare en brøkdel av hva det er å kunne. Det betyr ikke at du ikke har noe å bidra med! **Stup inn, og begynn reisen din som React-utvikler!** 18 | 19 | ### Ressurser du kan lese 20 | 21 | Det finnes utrolig mye god introduksjonslektyre om React allerede, og hvis du aldri har rørt React før, anbefaler vi at du tar en titt på noen av disse artiklene eller videokursene. Vi går igjennom noen av disse konseptene i workshopen, men om du tar denne workshopen på egenhånd, eller ikke har mulighet til å være fysisk tilstede, så anbefaler vi at du blar deg gjennom disse tre ressursene: 22 | 23 | - [Main Concepts](https://reactjs.org/docs/hello-world.html)-delen av React-dokumentasjonen er en fantastisk innføring i de mest grunnleggende konseptene i React. 24 | - [React Tutorial For Beginners](https://egghead.io/courses/the-beginner-s-guide-to-react)-kurset til Kent C. Dodds er en strålende introduksjon til hvordan React fungerer, og forklarer hva React faktisk gjør på en veldig enkel og grei måte. 25 | - [Den offisielle tutorialen](https://reactjs.org/tutorial/tutorial.html) til React er også en veldig lærerik opplevelse. 26 | 27 | [Slidesene fra workshop-introduksjonen finner du her](https://slides.com/markusra/introduksjon-til-react/fullscreen) 28 | 29 | ### React på 2 minutter 30 | 31 |
Klikk her for en rask introduksjon 32 | 33 | React baserer seg på konseptet om at brukergrensesnittet ditt er en funksjon av data. Gitt litt data, så vil React gi deg tilbake det samme brukergrensesnittet. React kaller denne dataen `props` (en forkortelse for properties). 34 | 35 | Med andre ord: 36 | 37 | ```js 38 | brukergrensesnitt = f(props); 39 | ``` 40 | 41 | React baserer seg på at hver bit av brukergrensesnittet ditt er en funksjon. Denne funksjonen tar et objekt med data - `props` - som argument, og returnerer et brukergrensesnitt tilbake. I React kaller man denne typen funksjon for en **komponent**. 42 | 43 | En komponent kan se slik ut: 44 | 45 | ```js 46 | function MinKomponent(props) { 47 | return

Hei verden

; 48 | } 49 | ``` 50 | 51 | , eller med en annen syntaks for å lage funksjoner: 52 | 53 | ```js 54 | const MinKomponent = (props) => { 55 | return

Hei verden

; 56 | }; 57 | ``` 58 | 59 | Synes du den HTML-lignende syntaksen er rar? Det er greit - den _er_ litt rar. Den heter JSX, og er en type XML som React bruker for å beskrive brukergrensesnitt. Man bruker et verktøy som heter [Babel](https://babeljs.io) til å gjøre det om til vanlig JavaScript. JSX er egentlig bare syntaktisk sukker for funksjonen `React.createElement`! Dette er samme komponent som over, i helt vanlig JavaScript: 60 | 61 | ```js 62 | const MinKomponent = (props) => { 63 | return React.createElement("h1", {}, "Hei verden"); 64 | }; 65 | ``` 66 | 67 | Du kommer nok sjelden til å skrive kode som dette for hånd, da det å bruke JSX er å foretrekke i så godt som alle situasjoner. Men nå vet du i alle fall hva som egentlig skjer! 68 | 69 | Du kan (og bør!) lese mer om JSX i [Reacts dokumentasjon](https://reactjs.org/docs/introducing-jsx.html). 70 | 71 | Dette er i svært korte trekk det grunnleggende du trenger å vite om React. I løpet av oppgavene kommer du til å møte på mange flere konsepter, som tilstand (state), sideeffekter og kontekster - men nå vet du i alle fall litt om det mest grunnleggende! 72 | 73 |
74 | 75 | # Om workshopen 76 | 77 | Workshopen består av et sett med oppgaver, som du kan løse lokalt på din egen maskin eller i en CodeSandbox. 78 | 79 | ## Jobbe i nettleser eller på din egen maskin? 80 | 81 | Du trenger ikke å ha noe innstallert for å komme i gang med denne workshopen - kun en nettleser. Gå inn på [denne CodeSandbox-lenken](https://codesandbox.io/s/github/bekk/react-intro), så får du opp en moderne kode-editor, en live-oppdatert readme og alt du trenger rett i nettleseren. 82 | 83 | **Vi anbefaler å jobbe i nettleseren**, så du slipper å bruke tid på oppsett og slikt. 84 | 85 | Hvis du allikevel _vil_ løse oppgavene lokalt, kreves det at du har `node` installert. Hvis du ikke har det, kan du laste det ned fra [nodejs.org](https://nodejs.org). 86 | Du trenger også `git`, som du kan laste ned [herifra](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git). 87 | Du vil måtte jobbbe litt i terminalen også. Om du ikke er så bevandret i den verdenen, kan vi anbefale [denne artikkelen](https://www.git-tower.com/learn/git/ebook/en/command-line/appendix/command-line-101). 88 | 89 | Hvis du vil gjøre oppgavene lokalt, kan du åpne terminalen din, laste ned repoet med `git clone git@github.com:bekk/react-intro.git` og kjøre `npm install`. Du kan starte en utviklingsserver ved å kjøre `npm start` - denne vil laste inn appen din på nytt hver gang du gjør en endring. 90 | 91 | ## Emoji-guide 92 | 93 | Du kommer til å se noen emojis i oppgavene. De betyr ca det her: 94 | 95 | - 🏆Oppgave: Her er hva du skal gjøre 96 | - 💡Tips: Litt ekstra info som kan være greit å være for å løse en oppgave 97 | - 🚨Løsningsforslag: Her finner du en komplett gjennomgang av hvordan du _kan_ løse oppgaven 98 | 99 | # Oppgaver 100 | 101 | I denne workshopen skal vi lage den neste SoMe-hypen: **Bekkstagram**! 🎉 102 | 103 | Appen kommer til å implementere en forenklet versjon av Instagram, hvor du kan legge ut bilder, og like og kommentere andre sine bilder. Og ta det med ro - vi gjør det hele steg for steg, med gode forklaringer i hver oppgave. 104 | 105 | Trenger du hjelp, så er det bare å rekke opp hånda. Husk – ingen spørsmål er for enkle, og det eneste dumme er å sitte og lure i 5 minutter før du spør! 106 | 107 | ## Del 1: React 101 - De grunnleggende byggesteinene! 108 | 109 | ### Oppgave 1: Hei verden! 110 | 111 | La oss starte litt enkelt, med å få appen vår til å skrive ut noe som helst. Gå inn i `src/App.js`, og se hva som er der. 112 | 113 | 🏆 Få React til å skrive ut en `

`-tag med teksten "Bekkstagram" inni. 114 | 115 |
🚨Løsningsforslag 116 | Vi gjør alt arbeidet vårt i `App`-funksjonen. 117 | 118 | ```js 119 | function App() { 120 | return

Bekkstagram

; 121 | } 122 | ``` 123 | 124 | Den HTML-lignende syntaksen er hva vi kaller JSX, og er egentlig bare en fin måte å skrive `React.createElement('h1', null, 'Bekkstagram');` på. 125 | 126 | Funksjonen `App` blir kjørt i `src/index.js`, her: 127 | 128 | ```js 129 | ReactDOM.render(, rootElement); 130 | ``` 131 | 132 | Her ber vi React plassere resultatet av hva `App`-funksjonen returnerer inn i `rootElement`, som er en `
`-tag i HTML-en vår. 133 | 134 |
135 | 136 | ### Oppgave 2: Din første komponent! 137 | 138 | React er egentlig bare en haug med funksjoner som returnerer JSX. Disse funksjonene kaller vi "komponenter". 139 | 140 | 🏆 Lag en ny komponent, `
`, som skriver ut en `

`-tag med teksten "Bekkstagram" inni, og bruk den i appen din. 141 | 142 | > 💡 En React-komponent er en funksjon som starter med `StorForbokstav`, og som returnerer litt JSX eller `null`. 143 | 144 |
🚨Løsningsforslag 145 | 146 | Vi flytter koden vi skrev i oppgave 1 til en funksjon vi kaller `Header`. `Header` er en komponent. 147 | 148 | ```js 149 | function Header() { 150 | return

Bekkstagram

; 151 | } 152 | ``` 153 | 154 | Vi kan bruke `
`-komponenten vår som om det var en vanlig HTML-tag! La oss bruke den nye komponenten vår i ``-komponenten vår: 155 | 156 | ```js 157 | function App() { 158 | return
; 159 | } 160 | ``` 161 | 162 | Det er en grei huskeregel at DOM-komponenter starter med liten forbokstav, og React-komponenter starter med Stor forbokstav. `
` er med andre ord et HTML-element, mens `Header` er en referanse til `Header`-funksjonen vi akkurat skrev. 163 | 164 | Det fine med komponenter er at de kan brukes gang på gang - du har laget noe som er gjenbrukbart!
165 | 166 | ### Oppgave 3: Bilde-komponent 167 | 168 | Komponenter er morsommere når man sender inn litt data. Som vi husker fra over, er komponenter bare funksjoner som returnerer litt JSX. Disse funksjonene blir kalt med ett argument - et objekt vi kaller `props` . 169 | 170 | Du sender inn props til en komponent ved å spesifisere dem som attributten på JSX-elementet - akkurat som på vanlig HTML. 171 | 172 | 🏆 Lag en ny komponent `` som tar i mot to props, `src` og `alt`, og som lager en ``-tag som bruker disse to propsa. 173 | 174 | 🏆 Legg på css-klassen `image` på ``-taggen, så får den tilogmed riktig design! 175 | 176 | > 💡 I React så skriver man `className` istedenfor `class` - det er fordi JSX egentlig er JavaScript, og `class` er et såkalt reservert ord i JavaScript. Det er litt irriterende i starten, men man vender seg fort til det. 177 | 178 |
🚨Løsningsforslag 179 | Alle komponenter mottar et objekt som første argument. Verdiene i dette objektet kaller vi `props`. Derifra kan vi hente ut attributtene vi sendte med komponenten vår! 180 | 181 | ```js 182 | function Image(props) { 183 | return {props.alt}; 184 | } 185 | ``` 186 | 187 | Merk også at vi skriver `className` istedenfor `class` når vi skal legge til en CSS-klasse. Det er fordi `className` er navnet på attributten man bruker for å sette klassenavn på en DOM-node i JavaScript (og det er sånn React fungerer). 188 | 189 | Når vi skal bruke komponenten vår, må vi sende med de props-ene som vi bruker. Det ser du vi har gjort allerede, i ``-komponenten vår: 190 | 191 | ```js 192 | Carlton from Fresh Prince in Bel Air is dancing 196 | ``` 197 | 198 | Husk at en prop kan være hva som helst - en tekststreng, et tall, et objekt, en liste og tilogmed en funksjon! 199 | 200 |
201 | 202 | ### Oppgave 4: En liste med bilder 203 | 204 | Det beste med komponenter er at man kan bruke dem gang på gang. I denne oppgaven skal du loope ut en liste med bilder, og bruke samme komponenten hver gang. 205 | 206 | 🏆 Skriv ut en liste med bilder til nettleseren. Listen finner du i `src/data/images.js`, og du kan bruke `Image`-komponenten du laget i forrige oppgave til å skrive ut bildet. Husk å sende inn riktige props! 207 | 208 | > 💡 Husk at du trenger å sende inn en _unik_ `key` prop for hvert element i lista - ellers klarer ikke React å oppdatere lista di riktig. 209 | 210 |
🚨Løsningsforslag 211 | 212 | Hvis vi vil utføre JavaScript inni JSX, så kan vi "escape" oss ut med krøllparanteser. I dette tilfellet vil vi loope gjennom alle URLene i `images`-arrayet, og bruke ``-komponenten vår til å vise frem dataen. 213 | 214 | En måte å loope gjennom en liste og "gjøre" dem om til React-komponenter kan være den innebygde array-metoden "map". Den tar i mot en funksjon som kjøres for hvert element i lista, hvor den får elementet som argument, og så returneres et React-element. 215 | 216 | ```js 217 | function App() { 218 | return ( 219 |
220 |
221 |
222 | {images.map((image) => ( 223 | {image.description} 224 | ))} 225 |
226 |
227 | ); 228 | } 229 | ``` 230 | 231 | Hvert bilde i `images`-arrayen er et objekt med tre verdier - `id`, `url` og `description`. Vi bruker `id` som `key`, siden vi antar at den er unik per bilde. `url` sender vi inn som `src`-prop og `description` høres perfekt ut som en alt-tekst. 232 | 233 | `key`-propen i React er viktig, og er påkrevd når du skal lage lister av ting på denne måten. Det er React sin måte å vite hva som endret seg, om listen skulle endre seg i fremtiden (om man skulle fjerne, legge til eller sortere, for eksempel). 234 | 235 | Et viktig poeng med keys i React er at de bare trenger å være unike innen én liste, ikke hele appen. Derfor kan du gjerne bruke enhver verdi som er unik innad i datasettet man lister ut. 236 | 237 | Om du lurer på keys og hvorfor man trenger dem, så kan vi anbefale denne artikkelen: 238 | https://dev.to/jtonzing/the-significance-of-react-keys---a-visual-explanation--56l7 239 | 240 |
241 | 242 | ### Oppgave 5: Sett sammen komponenter med children 243 | 244 | På tide å gjøre innleggene våre litt mer innholdsrike. 245 | 246 | 🏆 Wrap hver av `Image`-komponentene du lister ut i en `Post`-komponent. 247 | 248 | Post-komponenten skal skrive ut følgende DOM-struktur: 249 | 250 | ```html 251 |
252 |
...
253 | 254 |
...
255 |
256 | ``` 257 | 258 | Hvordan du får til nettopp det er opp til deg - men vi anbefaler at du bruker `children` prop-en. Du kan lese mer om `props.children` i [dokumentasjonen til React](https://reactjs.org/docs/jsx-in-depth.html#children-in-jsx). 259 | 260 | > 💡 Vi kan anbefale funksjonen [`formatDistanceToNow`](https://date-fns.org/v2.4.1/docs/formatDistanceToNow) fra biblioteket `date-fns` for å vise timestamp-informasjonen. 261 | 262 |
🚨Løsningsforslag 263 | `children` er en spesiell prop. Når du skrive koden din slik: 264 | 265 | ```js 266 | Hei og hallo 267 | ``` 268 | 269 | så dukker innholdet mellom taggene (i dette tilfellet "Hei og hallo") opp i denne prop-en. Med andre ord: `props.children === 'Hei og Hallo'`. 270 | 271 | Dette kan man bruke til å sette sammen flere komponenter, og lage hierarkier, slik som HTML har fra før av. 272 | 273 | I denne oppgaven skulle vi implementere tre komponenter. La oss ta en av gangen. 274 | 275 | ```js 276 | import formatDistanceToNow from "date-fns/formatDistanceToNow"; 277 | 278 | function Timestamp(props) { 279 | return ( 280 |
{formatDistanceToNow(props.timestamp)} ago
281 | ); 282 | } 283 | ``` 284 | 285 | Her er det ikke veldig mye nytt. Vi importerer og kaller funksjonen `formatDistanceToNow` for å gjøre om et dato-objekt til en tekststreng som beskriver hvor lenge siden tidspunktet var. 286 | 287 | ```js 288 | function Author(props) { 289 | return
{props.children}
; 290 | } 291 | ``` 292 | 293 | Her bruker vi `props.children` for første gang! Det betyr at vi plasserer hva enn man plasserer mellom `` og `` inni en `
` med et klassenavn på. Dette "hva enn" kan være en tekst, et tall eller mer JSX. 294 | 295 | ```js 296 | function Post(props) { 297 | return ( 298 |
299 | {props.author} 300 | {props.children} 301 | 302 |
303 | ); 304 | } 305 | ``` 306 | 307 | ``-komponenten vår bruker alt på en gang! Her sender vi inn `props.author` som `children`-propen til ``-komponenten, etterfulgt av at vi plasserer `Post`'s egne `props.children`-prop under. Til slutt plasserer vi ``-komponenten nederst, og videresender `createdDate`-propen. 308 | 309 | Hele ``-koden blir slik: 310 | 311 | ```js 312 | function App() { 313 | return ( 314 |
315 |
316 |
317 | {images.map((image) => ( 318 | 323 | {image.description} 324 | 325 | ))} 326 |
327 |
328 | ); 329 | } 330 | ``` 331 | 332 | Henger du med? Hvis ikke er det helt okei. Still spørsmål til de som går rundt og hjelper. 333 | 334 |
335 | 336 | ### Oppgave 6: Vis ett og ett bilde 337 | 338 | Alle de kule appene har forskjellige sider og URLer. Det burde vi også få oss. I denne oppgaven skal vi bruke biblioteket `react-router-dom` til å lage to forskjellige sider i applikasjonen vår - `FeedPage` og `DetailPage`. 339 | 340 | > 💡 Begynn med å ta en titt på [dokumentasjonen til React Router](https://reacttraining.com/react-router/web/guides/quick-start) for en rask introduksjon til de forskjellige funksjonene du finner der. 341 | 342 | 🏆 Lag to nye komponenter - `FeedPage` og `DetailPage`. `FeedPage` bør vise listen over bilder du hadde fra før av. `DetailPage` bør vise bildet som har IDen i URLen. 343 | 344 | Bruk `BrowserRouter`- og `Route`-komponentene fra `react-router-dom` til å spesifisere URLene de forskjellige sidene skal vises på. `FeedPage` bør vises på `/`, og `DetailPage` bør vises på `/post/:id`. 345 | 346 | > 💡 `/post/:id` er en såkalt dynamisk route. Den vil treffe alle URLer på formen `/post/1`, `/post/1337`, `/post/ett-eller-annet`. Du kan hente ut verdien av `:id` med funksjonen [`useParams()`](https://reacttraining.com/react-router/web/api/Hooks/useparams). 347 | 348 | 🏆 Legg på en lenke rundt hvert bilde, slik at man kan navigere til detalj-siden for det bildet. URLen burde være `/post/iden-til-det-bildet`. 349 | 350 | 🏆 Legg på en lenke i `

`-taggen, slik at man kan trykke på "logoen" for å returnere til feeden igjen. 351 | 352 | > 💡 For interne lenker er [`Link`](https://reacttraining.com/react-router/web/api/Link)-komponenten fra `react-router-dom` fin å bruke. For eksterne lenker ut av appen din kan du bruke vanlige ``-tags. 353 | 354 |
🚨Løsningsforslag 355 | I denne oppgaven skal vi introdusere routing - det å kunne ha flere forskjellige URLer, og vise forskjellig innhold på hver av sidene. 356 | 357 | Vi starter med å installere biblioteket `react-router-dom`, som er den mest populære måten å løse dette på i dag. Du kan finne dokumentasjonen på https://reacttraining.com/react-router/web/guides/quick-start. Det er allerede lagt til `package.json` for deg, så du trenger ikke slenge det på. 358 | 359 | Dette biblioteket er egentlig ganske enkelt. Man spesifiserer en komponent, og for hvilke URLer man vil at denne komponenten skal vises. 360 | 361 | Vi starter med å refaktorere koden som lister ut bilder i en ny komponent - ``. 362 | 363 | ```js 364 | function FeedPage() { 365 | return ( 366 |
367 | {images.map((image) => ( 368 | 369 | {image.description} 370 | 371 | ))} 372 |
373 | ); 374 | } 375 | 376 | function App() { 377 | return ( 378 |
379 |
380 | 381 |
382 | ); 383 | } 384 | ``` 385 | 386 | Dette ser jo egentlig ganske ryddig ut! Neste vi må gjøre er å wrappe hele App-komponenten vår i en ``-komponent. 387 | 388 | ```js 389 | import { BrowserRouter } from "react-router-dom"; 390 | 391 | function App() { 392 | return ( 393 | 394 |
395 |
396 | 397 |
398 |
399 | ); 400 | } 401 | ``` 402 | 403 | Vi skal vise to forskjellige sider - en på url-en "/" (altså på rotnivå), og en på urlen "/post/1", "/post/2" osv, avhengig av IDen til bildet vi skal vise. Vi kaller hver av disse to URLene en rute - eller route på engelsk. For å vise en komponent hvis URLen "matcher" `"/"`, for eksempel - trenger vi å bruke en ``-komponent (også fra `react-router-dom`-pakken): 404 | 405 | ```js 406 | import { BrowserRouter, Route } from "react-router-dom"; 407 | function App() { 408 | return ( 409 | 410 |
411 |
412 | 413 | 414 | 415 |
416 |
417 | ); 418 | } 419 | ``` 420 | 421 | Her sender vi inn `path` som er URLen vi vil "matche", `exact` for at vi bare vil vise denne siden når urlen er _eksakt_ "/", og så sender vi inn det vi vil vise som `children` når URLen matcher. 422 | 423 | Det gir ikke mye mening å bare ha en rute når man har en router, så la oss legge til detaljsiden også. Vi vil vise detaljsiden når URLen er "/post/1", "/post/2" osv - da kan vi bruke en såkalt "route parameter", og spesifisere path-en som "/path/:id". 424 | 425 | ```js 426 | function App() { 427 | return ( 428 | 429 |
430 |
431 | 432 | 433 | 434 | 435 | 436 | 437 |
438 |
439 | ); 440 | } 441 | ``` 442 | 443 | `DetailPage` ser ganske lik ut som `FeedPage`, bare at den lister ut en enkel side: 444 | 445 | ```js 446 | import { useParams } from "react-router-dom"; 447 | function DetailPage() { 448 | const { id } = useParams(); 449 | const image = images.find((image) => image.id === id); 450 | return ( 451 |
452 | 453 | {image.description} 454 | 455 |
456 | ); 457 | } 458 | ``` 459 | 460 | Som du ser av koden over, kan man hente ut verdien av `:id` fra funksjonen `useParams` - hvor `id` er `tekstenEtterKolon` i `path`-parameteret. Så bruker vi den IDen til å slå opp riktig element i `images`-arrayet. 461 | 462 | For at det skal være noe vits med slike ruter, trenger vi å lage noen lenker mellom dem også. Der må vi bruke nok en komponent fra `react-router-dom` - nemlig ``. Du kan se dokumentasjonen her: https://reacttraining.com/react-router/web/api/Link 463 | 464 | Vi lager to lenker - logoen vår i `
`-komponenten lenker til "/", og hvert bilde lenker til "/post/{iden-til-det-bildet}". Slik ser det ut: 465 | 466 | ```js 467 | import { Link } from "react-router-dom"; 468 | 469 | function Header(props) { 470 | return ( 471 |
472 |

473 | Bekkstagram 474 |

475 |
476 | ); 477 | } 478 | ``` 479 | 480 | ```js 481 | function FeedPage(props) { 482 | return ( 483 |
484 | {images.map((image) => ( 485 | 490 | 491 | {image.description} 492 | 493 | 494 | ))} 495 |
496 | ); 497 | } 498 | ``` 499 | 500 | > 💡 Legg merke til at vi bruker "bakoverfnutter" når vi setter sammen lenken i ``-propen. Dette kalles en "template string", og lar deg interpolere verdier i en string. Du kan lese mer om dem på [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals). 501 | 502 | Til sammen har vi nå en app med to "sider". En feed-side, som egentlig bare er en feed-komponent som bare vises når URLen er "/", og en detaljside, som er en detalj-komponent som bare vises når URLen er "/post/1" osv. 503 | 504 |
505 | 506 | ## Del 2: Tilstand og sideeffekter 507 | 508 | Mye av det vi har gjort til nå kunne vi fått til med mye mindre kode, og helt uten et rammeverk. Ingenting endrer seg jo! Heldigvis er det nettopp her React skinner. 509 | 510 | React har innebygget funksjonalitet for å huske på tilstand, fyre av side-effekter og masse annet. Denne funksjonaliteten kalles for "hooks". 511 | 512 | Du kan lese om hooks [her](https://reactjs.org/docs/hooks-intro.html), og finne et oppslagsverk [her](https://reactjs.org/docs/hooks-reference.html). Du trenger ikke lese gjennom det nå, men ha det gjerne tilgjengelig mens du løser oppgavene i del 2. 513 | 514 | > 💡 Synes du App.js-filen din begynner å bli litt lang? Nå kan det være en god ide å refaktorere den ut i flere forskjellige filer. Man kan plassere en komponent i en fil, eller ha flere relaterte i samme fil - eller bare ha alt i en eneste fil også. Her er det dessverre ingen gale svar - finn den strukturen som fungerer for deg! 515 | 516 | ### Oppgave 7: Legg til likes som lokal state på hvert bilde 517 | 518 | La oss gjøre Bekkstagram litt mer avhengighetsskapende ved å introdusere likes. Antall likes et bilde har kan ses på som en tilstand, og dette er en perfekt anledning til å ta i bruk hooken `useState`. 519 | 520 | 🏆 Bruk hooken `React.useState` til å holde styr på antall likes en post har fått. Den burde starte på 0. 521 | 522 | > 💡 Synes du syntaksen `const [enTing, enAnnenTing] = React.useState()` er litt rar? Dette kalles array-destrukturering, og det kan du lese mer om i [denne artikkelen](https://dev.to/sarah_chima/destructuring-assignment---arrays-16f). Kort forklart henter det ut de to første elementene i et array, og lagrer dem som konstanter med egne navn. 523 | 524 | 🏆 Lag en knapp som har en "👍" inni seg, og gi den klassen "like-button". 525 | 526 | > 💡 Send gjerne inn propen `aria-label` med en beskrivelse av hva knappen gjør også - da er det lettere for svaksynte å bruke appen din! 527 | 528 | 🏆 Når man trykker på knappen bør man oppdatere antall likes. 529 | 530 | > 💡 Du kan sende inn en funksjon til propen `onClick` som kjøres hver gang noen klikker på knappen. 531 | 532 | > 💡 Når man sender inn en funksjon, så må man huske på å _ikke_ kalle den med en gang! Med andre ord - istedenfor å skrive `onClick={handleClick()}`, så skriver du `onClick={handleClick}`. Når noen klikker på knappen vår, er det React sin jobb å kalle `handleClick`-funksjonen vår. 533 | 534 |
🚨 Løsningsforslag 535 | 536 | I oppgave 7 skulle vi implementere å like bilder. 537 | 538 | Vi starter med å lage en ny komponent - ``: 539 | 540 | ```js 541 | function Likes(props) { 542 | return
; 543 | } 544 | ``` 545 | 546 | Neste steg er å begynne å bruke den i ``-komponenten vår også: 547 | 548 | ```js 549 | export default function Post(props) { 550 | return ( 551 |
552 | {props.author} 553 | {props.children} 554 |
555 | 556 | {/* ⬅️ her!*/} 557 |
558 |
559 | ); 560 | } 561 | ``` 562 | 563 | Siden vi nå skal innføre en tilstand (state) i appen vår, trenger vi å bruke hooken `React.useState`. Denne funksjonen tar i mot et argument, som er den initielle verdien. Den returnerer et array, hvor første element er verdien (tilstanden), og andre element er en funksjon som oppdaterer verdien. Man kan bruke en teknikk som heter destrukturering til å lage to variabler av disse. 564 | 565 | ```js 566 | function Likes(props) { 567 | const [likes, setLikes] = React.useState(0); 568 | return
; 569 | } 570 | ``` 571 | 572 | Du kan også skrive det på denne måten om du vil: 573 | 574 | ```js 575 | const state = React.useState(0); 576 | const likes = state[0]; 577 | const setLikes = state[1]; 578 | ``` 579 | 580 | (men ikke gjør det - det er ikke like lett å lese). 581 | 582 | Neste steg er å vise antall likes: 583 | 584 | ```js 585 | function Likes(props) { 586 | const [likes, setLikes] = React.useState(0); 587 | return
Likes: {likes}
; 588 | } 589 | ``` 590 | 591 | Når vi ser på websiden, ser vi at det står "Likes: 0" 592 | 593 | Del to av oppgaven består i å lage en knapp man kan trykke på, og som legger til en til antall likes. 594 | 595 | ```js 596 | function Likes(props) { 597 | const [likes, setLikes] = React.useState(0); 598 | function incrementLikes() { 599 | setLikes(likes + 1); 600 | } 601 | return ( 602 |
603 | Likes: {likes}{" "} 604 | 607 |
608 | ); 609 | } 610 | ``` 611 | 612 | Vi lager først en ny funksjon `incrementLikes`, som kaller `setLikes`-funksjonen med antall likes + 1. Du kan også sende inn en funksjon som tar imot nåværende verdi, og som returnerer oppdatert verdi: 613 | 614 | ```js 615 | function incrementLikes() { 616 | setLikes((currentLikes) => currentLikes + 1); 617 | } 618 | ``` 619 | 620 | Man bør bruke sistnevnte om den nye verdien avhenger av den gamle verdien - for å garantere at ikke noe annet oppdaterer antall likes i mellomtiden. 621 | 622 | Det var det! Vi kan nå like bildene våre! Om det bare var en måte å la serveren vår huske det på... 623 | 624 |
625 | 626 | ### Oppgave 8: Sideeffekter 627 | 628 | React-komponenter er egentlig ganske vanlige "rene" eller "pure" funksjoner. De blir kalt med noen argumenter (props), og returnerer litt JSX (viewet vårt). Dette gjør React ganske enkelt å forstå seg på. Men av og til trenger vi å påvirke noe utenfor komponenten vår også. 629 | 630 | I oppgave 8 skal vi fokusere på å utføre forskjellige side-effekter. Side-effekter er handlinger som påvirker noe utenfor "React-verdenen" - som å kalle DOM-APIer, hente data og så videre. 631 | 632 | ### 8A: Oppdater tittel 633 | 634 | Når man går inn på et bilde burde man oppdatere tittelen til websiden (det som står oppe i fanen). 635 | 636 | 🏆 Bruk hooken `React.useEffect` til å oppdatere tittelen til å si "📷 av @brukernavn" når man går inn på en detaljside. 637 | 638 | > 💡 Du kan sette sidetittelen med å endre `document.title` 639 | 640 |
🚨 Løsningsforslag 641 | En side-effekt er noe som påvirker noe utenfor React-verdenen. Det kan være å kalle DOM-APIer, hente data eller noe helt annet. I dette tilfellet vil vi oppdatere dokument-tittelen - den tekststrengen som vises i nettleser-fanen. 642 | 643 | Vi bruker den innebygde hooken `React.useEffect` for å kjøre denne side-effekten inni komponenten vår. `useEffect` tar i mot en funksjon som skal utføre side-effektene for oss. Vi kan implementere det slik: 644 | 645 | ```js 646 | React.useEffect(() => { 647 | document.title = "Min nye tittel"; 648 | }); 649 | ``` 650 | 651 | I vårt tilfelle vil vi at tekst-strengen skal gjenspeile hvilken bruker som har lastet opp bildet. Det er ikke noe vanskeligere enn vanlig: 652 | 653 | ```js 654 | React.useEffect(() => { 655 | document.title = `📷 av ${image.username}`; 656 | }); 657 | ``` 658 | 659 | Når du navigerer fra ett bilde til et annet ser du at tittelen oppdaterer seg. Om du jobber i CodeSandbox, må du riktignok åpne panelet til høyre i ene egen fane for å se det.s 660 | 661 | Sluttresultatet ser slik ut: 662 | 663 | ```js 664 | function DetailPage(props) { 665 | const { id } = useParams(); 666 | const image = images.find((image) => image.id === id); 667 | React.useEffect(() => { 668 | document.title = `📷 av ${image.username}`; 669 | }); 670 | return ( 671 |
672 | 673 | {image.description} 674 | 675 |
676 | ); 677 | } 678 | ``` 679 | 680 |
681 | 682 | ### 8B: Oppdater tittel (del 2) 683 | 684 | Oppgave 8A innførte en liten bug - når man returnerer til feed-siden (hovedsiden) resetter man ikke tittelen! Det bør vi gjøre noe med. Refaktorer ut en funksjon som setter tittelen for deg, og kall den `useTitle`. Dette er hva man kaller en [custom hook](https://reactjs.org/docs/hooks-custom.html). 685 | 686 | > 💡 En custom hook er bare en helt vanlig funksjon som starter med `use`, og som kaller en eller flere hooks. Det er ikke noe mer magi! 687 | 688 | > 💡 Husker du `useParams` fra da vi satt opp routing i oppgave 6? Det er en custom hook det også! 689 | 690 | 🏆 Bruk din første egenlagde custom hook både på `DetailPage` og `FeedPage`. 691 | 692 |
🚨 Løsningsforslag 693 | Denne oppgaven er nesten bare copy paste. 694 | 695 | Vi lager en ny fil - `useTitle.js`, og fyller inn følgende: 696 | 697 | ```js 698 | import React from "react"; 699 | 700 | export default function useTitle(title) { 701 | React.useEffect(() => { 702 | document.title = title; 703 | }); 704 | } 705 | ``` 706 | 707 | Eller som pilfunksjon: 708 | 709 | ```js 710 | import React from "react"; 711 | 712 | const useTitle = (title) => { 713 | React.useEffect(() => { 714 | document.title = title; 715 | }); 716 | }; 717 | 718 | export default useTitle; 719 | ``` 720 | 721 | Med andre ord så lager vi en funksjon som kaller en hook. Dette er hva man kaller en custom hook. 722 | 723 | Vi kan nå endre koden vår i `DetailPage` til å kalle den nye hooken vår: 724 | 725 | ```js 726 | import useTitle from './useTitle'; 727 | 728 | export default function DetailPage(props) { 729 | const image = images.find( 730 | image => image.id === Number(props.match.params.id), 731 | ); 732 | useTitle(`📷 av ${image.username}`); 733 | return (...); 734 | ``` 735 | 736 | Vi kan også lett bruke samme funksjonalitet i `FeedPage`: 737 | 738 | ```js 739 | import useTitle from './useTitle`; 740 | 741 | export default function FeedPage(props) { 742 | useTitle(`Bekkstagram`); 743 | return (...) 744 | } 745 | ``` 746 | 747 |
748 | 749 | ### 8C: Oppdater tittel (del 3) 750 | 751 | Custom Hooken vår ser fin ut - men den setter tittelen hver eneste gang vi rendrer siden vår. Det er kanskje ikke noe problem akkurat nå - men det kan det fort bli. 752 | 753 | 🏆 Oppdater `useTitle` med et `dependency array` som andre argument. ([Her er dokumentasjonen](https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects)) 754 | 755 | > 💡 Konseptuelt så kan du tenke på dependency-arrayet som en liste over ting, som, hvis de endres, krever at man kjører funksjonen som sendes inn til `React.useEffect` en gang til. Man kan kalle det å "synkronisere en effekt med tilstanden". Lettere forklart: Om en ting i dependency-arrayet endrer seg, kjør funksjonen på nytt med nye verdier. 756 | 757 |
🚨 Løsningsforslag 758 | Det eneste vi trenger å gjøre her er å legge til et array som andre argument i useEffect. Bruker man et tomt array trigges useEffect kun ved første render. Vi vil derimot at useEffect trigges hver gang `title` endrer seg, derfor legger vi `title` inni arrayet. 759 | 760 | ```js 761 | export default function useTitle(title) { 762 | React.useEffect(() => { 763 | document.title = title; 764 | }, [title]); 765 | } 766 | ``` 767 | 768 |
769 | 770 | ### Oppgave 9: Hent data fra backenden 771 | 772 | Akkurat nå leser vi bare statisk data som vi har hardkodet inn i appen. La oss hente data fra APIet vårt! 773 | 774 | Du kan kalle den asynkrone funksjonen `getFeed` fra `./server`-filen i prosjektet. Den returnerer et [Promise](https://medium.com/@PangaraWorld/an-introduction-to-understanding-javascript-promises-37eff85b2b08), som etterhvert returnerer en liste med bilder. 775 | 776 | 🏆 Hent en liste med bilder med `getFeed` funksjonen, og list dem ut på `FeedPage`. 777 | 778 | > 💡 Du kan bruke `useEffect` til å hente data fra serveren. Husk å bare hente ny data når det trengs - i vårt tilfelle er det bare når vi laster siden! 779 | 780 | > 💡 For å bare kjøre `useEffect` når man laster siden, så kan du spesifisere et tomt dependency array 781 | 782 | > 💡 Du kan lagre dataen i en `useState`-hook. 783 | 784 |
🚨 Løsningsforslag 785 | 786 | For å hente bildene lager vi en ny custom hook `useFeed` som kan implementeres slik: 787 | 788 | ```js 789 | import { getFeed } from "./server"; 790 | 791 | const useFeed = () => { 792 | const [images, setImages] = React.useState(null); 793 | React.useEffect(() => { 794 | getFeed().then((data) => setImages(data)); 795 | }, []); 796 | return images; 797 | }; 798 | ``` 799 | 800 | Denne hooken bruker `getFeed` metoden til APIet vårt for å hente alle bildene i feeden vår. Når serveren har sendt oss dataene, kalles funksjonen inni `then` - og der oppdaterer vi staten vår med den dataen. 801 | 802 | > Denne måten å uttrykke asynkronitet - eller det å vente på noe - på, heter promises. Du kan lese litt mer om dem her om du er interessert: https://medium.com/@PangaraWorld/an-introduction-to-understanding-javascript-promises-37eff85b2b08 803 | 804 | I denne custom hooken bruker vi flere hooks på en gang - både `useEffect` og `useState`. Det er helt innafor - og noe man gjør ganske ofte. 805 | 806 | Vi sender inn et tomt array som andre argument til `useEffect`. Det betyr at denne sideeffekten kun skal kjøres en gang - når siden rendres for første gang. Vi vil jo bare hente listen over bilder når man går inn på siden - ikke hver gang man liker et bilde! 807 | 808 | I slutten av custom hooken vår returnerer vi bildene våre. Første gang siden lastes vil denne verdien være `null`, og når dataen har blitt lagret, vil verdien være en liste av bildedetaljer. 809 | 810 | I `FeedPage`-komponenten kan vi sette `images`-konstanten til å være lik resultatet fra `useFeed`. 811 | 812 | ```js 813 | const images = useFeed(); 814 | ``` 815 | 816 | Siden `images` kan være `null` nå, så er det viktig at vi sjekker om vi har bilder eller ei. Det kan vi gjøre slik: 817 | 818 | ```js 819 | const images = useFeed(); 820 | 821 | if (!images) { 822 | return null; 823 | } 824 | ``` 825 | 826 | Hvis du vil så kan du implementere en spinner her også - men det lar vi være en ekstraoppgave for den spesielt interesserte. 827 | 828 | På samme måte kan vi lage en custom hook som henter akkurat det bildet du klikker deg inn på. Her legger vi også til et dependency array basert på bilde ID'en, slik at 'useImage' som bruker 'getImage', kjører hvis ID'en endrer seg. 829 | 830 | ```js 831 | import { getImage } from "./server"; 832 | 833 | const useImage = (id) => { 834 | const [image, setImage] = React.useState(null); 835 | React.useEffect(() => { 836 | getImage(id).then((data) => setImage(data)); 837 | }, [id]); 838 | return image; 839 | }; 840 | ``` 841 | 842 | En måte å tenke på `useEffect` er at den synkroniserer tilstand basert på de verdiene du plasserer i dependency arrayet. Hvis en verdi i den lista endrer seg, vel, da må side-effekten kjøres en gang til for at alt skal være riktig. 843 | 844 | Denne henter vi i DetailPage komponenten vår. 845 | 846 | ```js 847 | const image = useImage(id); 848 | ``` 849 | 850 | Det samme gjelder her - om vi får tilbake `null` fra `useImage`, så må vi passe på å returnere `null` fra komponenten vår også. 851 | 852 |
853 | 854 | ### Oppgave 10: Legg til bilder! 855 | 856 | Ingen bilder er like kule som sine egne. I denne oppgaven skal du prøve å laste opp dine egne. 857 | 858 | > 💡 Før du begynner denne oppgaven så anbefaler vi at du åpner filen `sett-brukernavnet-ditt-her.js`", og gir deg selv et unikt brukernavn! 859 | 860 | For å gjøre det enkelt, lar vi deg kun legge til bilder som allerede ligger på internett. Finn en URL til et bilde du har rettighetene til, og vis det på siden! 861 | 862 | 🏆 Lag et brukergrensesnitt for å legge til bilder. Vi trenger en URL og en beskrivelse. Bruk `uploadImage`-funksjonen fra `./server`-filen for å laste opp bilder. 863 | 864 | > 💡 Funksjonen `uploadImage` tar imot et objekt som argument: 865 | > 866 | > ```js 867 | > import { uploadImage } from "./server"; 868 | > // ... 869 | > uploadImage({ 870 | > url: "https://placekitten.com/600/400", 871 | > description: "A very cute kitten", 872 | > }); 873 | > ``` 874 | 875 | 🏆 Legg til en knapp på siden for å vise "legg til bilde"-grensesnittet ditt. Du kan f.eks. vise denne brukerinputen med pakken `@reach/dialog`, eller skrive din egen. 876 | 877 | > 💡 @reach/dialog er en ferdig installert pakke i dette prosjektet. Du finner dokumentasjonen til @reach/dialog på [hjemmesiden deres](https://ui.reach.tech/dialog/) 878 | > 879 | > ```js 880 | > import { Dialog } from "@reach/dialog"; 881 | > import "@reach/dialog/styles.css"; 882 | > ``` 883 | 884 | > 💡 Importer et bildeikon av et kamera for å bruke som legg-til-bilde-knapp fra [react-icons](https://www.npmjs.com/package/react-icons), her et ikon fra [Font Awesome](https://fontawesome.com/icons?d=gallery&q=camera): 885 | > 886 | > ```js 887 | > import { FaCameraRetro } from "react-icons/fa"; 888 | > ``` 889 | > 890 | > Du kan også legge på klassen "camera-button" på knappen din for å få den til å se pen ut, og dukke opp nede i hjørnet :) 891 | 892 | 🏆 Hvis du sender inn en ugyldig URL til `uploadImage` vil den throwe en exception. Hvis dette skjer, si ifra til brukeren, og da dem prøve igjen! 893 | 894 | 🏆 Sørg for at bildefeeden refresher seg og oppdateres med det nye bildet etter at det har blitt lagt til. 895 | 896 |
🚨 Løsningsforslag 897 | 898 | Denne oppgaven kan nok løses på flere måter, men vi har valgt å implementere en knapp som åpner en modal/dialog med to input-felter hvor man kan skrive inn en bildeurl og en beskrivelse. Det meste er laget i en ny `AddImage`-komponent. 899 | 900 | Selve knappen vi har brukt er bare et ikon av et kamera vi har hentet fra et ekstern bibliotek, og kan importeres (som en komponent) slik: 901 | 902 | ```js 903 | import { FaCameraRetro } from "react-icons/fa"; 904 | ``` 905 | 906 | Denne har en `onClick`-prop som vi kan bruke for å åpne dialogen vi vil skal dukke opp. Vi kan importere en veldig fin Dialog-komponent fra biblioteket "reach" slik: 907 | 908 | ```js 909 | import { Dialog } from "@reach/dialog"; 910 | ``` 911 | 912 | Dialog-komponenten har en del props, deriblant `isOpen` og `onDismiss`, som det er naturlig å styre med en state i `AddImage`-komponenten vår. Et par states til er også naturlig å ha for å lagre url'en og beskrivelsen som man etterhvert skriver inn i input-feltene: 913 | 914 | ```js 915 | const [showDialog, setShowDialog] = React.useState(false); 916 | const [imageUrl, setImageUrl] = React.useState(""); 917 | const [imageDescription, setImageDescription] = React.useState(""); 918 | ``` 919 | 920 | `isOpen`-propen til Dialog kan da settes til `showDialog` og `onDismiss` kaller `setShowDialog(false)`. 921 | 922 | Alt innholdet i dialogen sendes inn som `children` til `Dialog`-komponenten. Det som dialogen blant annet må inneholde er en knapp som fyrer avgårde et api-kall til backenden for å lagre bilde med url'en og beskrivelsen som er spesifisert. Dette kan man gjøre direkte, f. eks bare: 923 | 924 | ```js 925 | 928 | ``` 929 | 930 | Men da vil ikke feeden oppdatere seg automatisk. vil oppdateres hvis staten oppdateres. Hvis vi dermed legger bildene i en state og lager en funksjon for å legge til et bilde til staten, kan vi sende denne funksjonen ned til ``-komponenten og kalle denne herfra etter å ha sendt bildet til backenden med api'et (`uploadImage`-metoden vil returnere det nye bilde-objektet som har blitt lagt til). Da vil staten til `` oppdateres med det nye bildet og komponenten vil rendres på nytt med det nye bildet. 931 | 932 | Endringene som da kan gjøres i ``: 933 | 934 | ```js 935 | const [images, setImages] = React.useState(null); 936 | 937 | const imagesFromFeed = useFeed(); 938 | 939 | React.useEffect(() => { 940 | setImages(imagesFromFeed); 941 | }, [imagesFromFeed]); 942 | 943 | const onAddImage = (image) => { 944 | setImages((prevImages) => [...prevImages, image]); 945 | }; 946 | ``` 947 | 948 | Samt legge til ``-komponenten helt nederst i ``: 949 | 950 | ```js 951 | 952 | ``` 953 | 954 | Hele den nye ``-komponenten: 955 | 956 | ```js 957 | import React from 'react'; 958 | import { uploadImage } from './server'; 959 | import { FaCameraRetro } from 'react-icons/fa'; 960 | import { Dialog } from '@reach/dialog'; 961 | 962 | export const AddImage = props => { 963 | const [showDialog, setShowDialog] = React.useState(false); 964 | const [imageUrl, setImageUrl] = React.useState(''); 965 | const [imageDescription, setImageDescription] = React.useState(''); 966 | 967 | const addImage = async (url, description) => { 968 | const imageResponse = await uploadImage({ 969 | url, 970 | description, 971 | }); 972 | if (!imageResponse) { 973 | return; 974 | } 975 | props.onAddImage(imageResponse); 976 | resetAndCloseDialog(); 977 | }; 978 | 979 | const resetAndCloseDialog = () => { 980 | setImageUrl(''); 981 | setImageDescription(''); 982 | setShowDialog(false); 983 | }; 984 | 985 | return ( 986 |
987 | 990 | setShowDialog(false)} 994 | > 995 |

Publiser et nytt bilde

996 | setImageUrl(event.target.value)} 1000 | placeholder="Url'en til bildet..." 1001 | /> 1002 | setImageDescription(event.target.value)} 1006 | placeholder="Bildebeskrivelse..." 1007 | /> 1008 | {imageUrl.length > 0 ? ( 1009 | 1015 | ) : ( 1016 | 1019 | )} 1020 | 1021 | 1029 |
1030 |
1031 | ); 1032 | }; 1033 | ``` 1034 | 1035 |
1036 | 1037 | ### Oppgave 11: Kommentarer 1038 | 1039 | På tide å legge til det morsomste med internett: kommentarfelt! Både mulighet for å vise kommentarer og legge til nye. Kommentarer ligger lagret som et array på hvert bildeobjekt som vi hentet fra backend i oppgave 9, så vi har allerede tilgang til det som ligger lagret i databasen fra før. Her er det bare å eksperimentere med nye komponenter og gjenbruke det dere hittil har lært! ` 1155 | 1156 | ); 1157 | }; 1158 | ``` 1159 | 1160 | Du legger kanskje merke til at du ikke får opp kommentaren du la til før du refreshet siden? 1161 | 1162 | Vi kan løse dette ved å innføre state i `` og lage en `addComment`-funksjon som setter denne staten, som vi igjen sender med til ``-komponenten som kan kalle denne funksjonen når vi legger til en kommentar. Istedenfor å rendre propsene `` mottar direkte rendrer vi heller denne staten. Derfor, når `` endrer staten til ``, vil det trigge en re-render av `` med oppdatert comments-array siden staten har endret seg. Ved bruk av hooks/useState trigges det nemlig en re-render av komponenten når staten endres. 1163 | 1164 | I `Comments.js`: 1165 | 1166 | ```js 1167 | export const Comments = (props) => { 1168 | const [comments, setComments] = React.useState(props.comments); 1169 | 1170 | const addComment = (comment) => { 1171 | setComments((prevState) => [...prevState, comment]); 1172 | }; 1173 | 1174 | if (comments) { 1175 | return ( 1176 |
1177 | {comments.map((comment, key) => ( 1178 | 1179 | ))} 1180 | addComment(comment)} 1182 | imageId={props.imageId} 1183 | /> 1184 |
1185 | ); 1186 | } 1187 | 1188 | return ; 1189 | }; 1190 | ``` 1191 | 1192 | Endre `onCommentSubmit()` i `` til. 1193 | 1194 | ```js 1195 | async function onCommentSubmit() { 1196 | const commentsResponse = await putComment(props.imageId, comment); 1197 | props.addComment(commentsResponse); 1198 | setComment(""); 1199 | } 1200 | ``` 1201 | 1202 | Viktig å merke seg await'en, siden `putComment()` er en async funksjon må vi vente på svar før vi fortsetter. 1203 | 1204 | 1205 | 1206 | ## Ekstraoppgaver 1207 | 1208 | Vi har endel ekstraoppgaver som du kan bryne deg på om du får tid, eller om du trenger noen ekstra utfordringer på et senere tidspunkt. 1209 | 1210 | Vi har laget et API som har støtte for mye rart. Ta en titt på [koden om du vil](https://github.com/markusra/bekkstagram-api). Oppdater likes til backend, eller hva du vil egentlig :) 1211 | 1212 | - Vis bildebeskrivelsen under hvert bilde 1213 | - Implementer at man bare kan like ett bilde per bruker 1214 | - Implementer støtte for å lagre et like til serveren 1215 | - Legg til støtte for hashtags 1216 | - List ut alle bildene til en bruker 1217 | - Søk etter innhold basert på hashtags, beskrivelser, brukernavn osv 1218 | - Legg til paginering (hent litt og litt bilder) 1219 | 1220 | Om du vil prøve deg på noe helt nytt, så har vi et par ideer du kan bryne deg på her: 1221 | 1222 | - **Lag din egen todo-liste!** Det er kanskje et utbrukt eksempel, men det er en fin måte å lære seg React på. Legg til støtte for å legge til todos, si at du er ferdig med dem, og filtrer ut både ferdige og gjenstående gjøremål. 1223 | - **Lag din egen PokeDex!** Om du fortsatt er glad i Pokemon, så kan du jo bruke [PokeAPI](https://pokeapi.co/) som datakilde, og lage din helt egne PokeDex! Legg til støtte for å liste ut alle pokemons, og å se detaljer om en spesifikk en. 1224 | - **Lær deg litt om animasjon i React!** Vi har laget en animasjonsworkshop også, der du lærer å bruke animasjonsrammeverket Framer Motion. Den finner du [her](https://github.com/bekk/react-animation-workshop) 1225 | - **Prøv deg på TypeScript!** TypeScript er en måte å bringe statisk typesikkerhet til React-koden din. Vi har selvfølgelig laget en workshop her også, som du finner [her](https://github.com/bekk/typet-javascript-workshop) 1226 | - [Front-end Mentor](https://www.frontendmentor.io/challenges) er en fin side å finne prosjekter du kan implementere. Der får du et ferdig design, og en spec på en oppgave 1227 | 1228 | Lykke til! 1229 | --------------------------------------------------------------------------------