ProgramowanieBudujemy aplikację
#aplikacja#projekt#kodowanie#nagłówek#header#komponenty#react#styled components
Posted on 3.02.2025

W poprzednim kroku napisaliśmy pierwsze linijki właściwego kodu naszej aplikacji. Dziś będziemy kontynuować kodowanie - konkretnie stworzymy komponent nagłówka!
Prace zakulisowe
Zanim przejdziemy dalej wspomnę, że w międzyczasie nieco uzupełniłem globalny styl - konkretnie dodałem grafikę tła aplikacji. Przeanalizujmy sobie to pokrótce.
Na samym początku utworzyłem w katalogu assets
podkatalog images, w którym umieściłem plik tła. Możecie je znaleźć w projekcie.
Czas przejść do kodu. Otwieramy plik src/utils/lib/styles/GlobalStyle.ts
i do znacznika body dodajemy poniższą linijkę:
background: linear-gradient(rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0.6)),
url(${background.src}) no-repeat center/cover fixed;
Aby wszystko zadziałało poprawnie, musimy zaimportować naszą grafikę. Teraz całość wygląda następująco:
src/utils/lib/styles/GlobalStyle.ts
import { createGlobalStyle } from 'styled-components'
import background from '@/assets/images/bgr.jpg'
const GlobalStyle = createGlobalStyle<{
$isDark?: boolean
}>`
*, *::after, *::before {
box-sizing: border-box;
}
html {
font-size: 62.5%;
font-family: ${({ theme }) => theme.fonts.main};
}
body {
margin: 0;
padding: 0;
line-height: 2;
font-size: ${({ theme }) => theme.fontSize.m};
color: ${({ theme, $isDark }) =>
$isDark ? theme.colors.white : theme.colors.black};
background: linear-gradient(rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0.6)),
url(${background.src}) no-repeat center/cover fixed;
}
h1, h2, h3, h4, h5, h6 {
margin: 0;
line-height: 1.5;
}
`
export default GlobalStyle
Powinno to prezentować się tak:
Dobrze, możemy teraz przejść do naszego headera!
Piszemy pierwszy komponent
Jak zwykle zaczniemy od utworzenia nowego brancha - u mnie jest to create-header
(który można jak zawsze podejrzeć).
Dobrze, przejdźmy szybko do katalogu src/components/
. Zastanówmy się, jakiego rodzaju komponentem będzie nasz Header. Spójrzmy na nasz projekt. Header będzie zawierał logo oraz nazwę aplikacji, a także przycisk do zmiany trybu jasny/ciemny i do wyboru języka. Można więc spokojnie założyć, że nasz Header będzie organizmem. Zatem w katalogu Organisms
tworzymy plik Header/Header.tsx
. Piszemy podstawę komponentu. I tutaj mamy do wyboru dwie opcje: napisanie komponentu z użyciem function
lub jako funkcji strzałkowej. Podobnie na dwa sposoby możemy nasz komponent wyekspotrować - jako named export lub default export. Warto w tym miejscu ustalić sobie stały schemat, którego będziemy się trzymać do końca. Ogólnie warto ujednolicić to, w jaki sposób piszemy kod. Sprawi to, że nasza aplikacja będzie czytelniejsza, bardziej przewidywalna i łatwiejsza w utrzymaniu.
W tym miejscu pokażę wam wszystkie możliwości, później zaś będę się już trzymał następującej konwencji. Komponenty tworzę za pomocą funkcji strzałkowych (ot, tak się kiedyś przyzwyczaiłem) i eksportuję je za pomocą named export. Natomiast funkcje pomocnicze będą eksportowane przy użyciu default export.
Ale do rzeczy - zaczynamy pisać nasz komponent!
Header.tsx
- function
z named export
function Header() {}
export default Header
Header.tsx
- function
z default export
export function Header() {}
Header.tsx
- funkcja strzałkowa z named export
const Header = () => {}
export default Header
Header.tsx
- funkcja strzałkowa z default export
export const Header = () => {}
Jak wspomniałem - ja będę używał opcji funkcji strzałkowej z named export.
Dobrze, mamy zarys (baaardzo podstawowy) komponentu. Spójrzmy, jak on wygląda w projekcie oraz z jakich elementów się składa.
Czerwonymi ramkami mamy oznaczone główne elementy komponentu, niebieskimi - elementy dalsze. Można więc w tym momencie założyć, że Header będzie się składał z dwóch mniejszych komponentów, w skład których wejdą kolejne cztery. Jeśli chcecie, możecie już w tym miejscu zacząć je tworzyć - jednak ja będę robił nieco inaczej. W pierwszej kolejności stworzę pełny komponent Header tak, jakby był atomem - czyli kompletnym, niepodzielnym elementem. Później dopiero zastanowię się, jak najsensowniej podzielić do na mniejsze elementy i wtedy przeniosę je do osobnych plików. Jednak jak wspomniałem - wy możecie już w tym momencie zacząć tworzyć docelowe komponenty.
Jedziemy!
Na początku stwórzmy jeszcze podkatalog images w katalogu src/assets
i wrzućmy tam ikony logo oraz wyboru języków (do pobrania z projektu). Dodatkowo do katalogu src/assets/images/
wrzućmy tymczasowy plik z ustawionym na sztywno wyborem trybu jasny/ciemny (także do pobrania z projektu). Teraz przejdźmy do naszego komponentu.
src/components/Organisms/Header/Header.tsx
import Image from 'next/image'
import Logo from '@/assets/icons/logoLight.svg'
import TempSwitcher from '@/assets/images/tempSwitcher.png'
import LangPicker from '@/assets/icons/langPicker.svg'
const Header = () => {
return (
<header>
<div>
<Image src={TempSwitcher} alt="" />
<Image src={LangPicker} alt="Pick language" />
</div>
<div>
<Image src={Logo} alt="" />
<h1>Smoggy Foggy</h1>
</div>
</header>
)
}
export default Header
I teraz po kolei.
Na początku importujemy grafiki oraz komponent Image
udostępniony przez bibliotekę Next. W samym komponencie - na razie - zwyczajnie wrzucamy nasze elementy w sposób, jaki wydaje nam się w tym momencie najbardziej uzasadniony. Co to znaczy "najbardziej uzasadniony"? Taki, jaki ustaliliśmy nieco wyżej, czyli dwa główne elementy (w tej chwili może to być div
), które zawierają kolejne dwa elementy każdy.
To co? Możemy podejrzeć zmiany? Oczywiście, że nie. Nasz komponent musimy jeszcze zaimportować. Przechodzimy szybko do pliku src/app/layout.tsx
. Tam importujemy nasz Header oraz umieszczamy go w szablonie:
import type { Metadata } from 'next'
import { ReactNode } from 'react'
import AppProviders from '@/utils/lib/providers/AppProviders'
import Header from '@/components/Organisms/Header/Header' //importujemy Header
export const metadata: Metadata = {
title: 'Smoggy Foggy',
description: 'Sprawdź jakość powietrza',
}
const RootLayout = ({
children,
}: Readonly<{
children: ReactNode
}>) => {
return (
<AppProviders>
<html lang="en">
<body>
<Header /> //wrzucamy Header
{children}
</body>
</html>
</AppProviders>
)
}
export default RootLayout
Pozostało jeszcze odpalić serwer developerski poleceniem npm run dev
w konsoli i podziwiać, jak paskudnie wygląda nasza aplikacja 😋
Stylujemy Header
Zacznijmy od ostylowania głównej części Headera. Zgodnie z projektem powinien się składać z dwóch elementów leżących jeden nad drugim. Na ten moment wystarczy zatem dać komponentowi jakiś margines i ustalić szerokość.
Ponownie - można już teraz stworzyć plik dla stylowanego komponentu, lub można też cały czas jeszcze pracować jeszcze w głównym pliku. Ja pozostanę przy tej drugiej opcji.
W górnej części naszego pliku, ponad deklaracją komponentu Header, utworzę nowy element.
const StyledHeader = styled.header`
width: 100%;
margin: 0 1rem 1rem;
`
Nic szczególnego, ale już sprawia, że całość wygląda NIECO lepiej. Poprawmy jeszcze widok samych elementów składających się na Header. Tam już podelementy powinny układać się obok siebie. Aby to uzyskać ostylujemy wewnętrzne div
-y Headera i nadamy im własność display: flex
. Dokładniejszym stylowaniem div
-ów zajmiemy się za chwilę, na razie tyle powinno nam wystarczyć.
const StyledHeader = styled.header`
width: 100%;
margin: 0 1rem 1rem;
div {
display: flex;
align-items: center;
}
`
Dobrze, ale nadal bardzo problematyczna jest wielkość naszych ikon. Zajmijmy się w pierwszej kolejności logo. Stwórzmy nowy styled component dla tego elementu.
const StyledLogo = styled(Image)`
height: 5rem;
width: 5rem;
margin-top: 6rem;
margin-right: 1.5rem;
`
Dobrze. Ustaliliśmy wielkość logo oraz jego położenie. Zabierzmy się teraz za tytuł.
const Title = styled.h1`
font-family: ${({ theme }) => theme.fonts.header};
margin: 6rem 0 0;
font-size: 4rem;
`
Czas zaimplementować nasze style w komponencie Header
src/components/Organisms/Header/Header.tsx
const Header = () => {
return (
<StyledHeader>
<div>
<Image src={TempSwitcher} alt="" />
<Image src={LangPicker} alt="Pick language" />
</div>
<div>
<StyledLogo src={Logo} alt="" />
<Title>Smoggy Foggy</Title>
</div>
</StyledHeader>
)
}
Jak widzimy, dolna połowa Headera wygląda już praktycznie tak, jak powinna. Popracujmy zatem chwilę nad górną.
Podstawową różnicą, jaką widzimy pomiędzy górną i dolną częścią jest umiejscowienie ich elementów. Dolna część jest wyrównana do lewej strony, tymczasem w górnej elementy są rozstrzelone. Tym jednak zajmiemy się później. W tej chwili ostylujemy górne ikonki.
const StyledLangIcon = styled.div`
position: relative;
margin: 0.5rem;
border-radius: 50%;
background: url(${LangPicker.src}) no-repeat center/cover;
height: 3rem;
width: 3rem;
border: 0;
z-index: 2;
`
Nieco zmieniłem podejście. Teraz ikona nie jest grafiką samą w sobie, lecz div
-em z grafiką w tle. Będzie to przydatne, kiedy zaczniemy implementować właściwy wybór języków.
div
z ikoną ma stałą wielkość i format kwadratu oraz zaokrąglenie 50% - co w konsekwencji nadaje mu kształt koła. Własność z-index
będzie przydatna w przyszłości.
Tymczasowa grafika, której użyliśmy zamiast przełącznika trybu, ma odpowiedni rozmiar, zatem tego elementu nie musimy (w tej chwili!) stylować.
Wróćmy zatem do rozmieszczenia elementów w górnej części Headera.
Proponuję utworzyć komponent Wrapper, który będziemy mogli dostosowywać za pomocą propsów, czyli własności przekazywanych bezpośrednio do komponentu. Uwaga, będzie się działo!
const StyledWrapper = styled.div<{
$position?: 'static' | 'relative' | 'absolute' | 'sticky' | 'fixed'
$m?: string | number
$p?: string | number
$display?:
| 'block'
| 'inline'
| 'inline-block'
| 'flex'
| 'inline-flex'
| 'grid'
| 'inline-grid'
| 'flow-root'
$justifyContent?:
| 'center'
| 'start'
| 'end'
| 'flex-start'
| 'flex-end'
| 'left'
| 'right'
| 'space-between'
| 'space-around'
| 'space-evenly'
| 'stretch'
$alignItems?:
| 'center'
| 'start'
| 'end'
| 'flex-start'
| 'flex-end'
| 'self-start'
| 'self-end'
| 'anchor-center'
}>`
position: ${({ $position }) => $position || 'static'};
margin: ${({ $m }) => $m || 0};
padding: ${({ $p }) => $p || 0};
display: ${({ $display }) => $display || 'initial'};
justify-content: ${({ $justifyContent }) => $justifyContent || 'unset'};
align-items: ${({ $alignItems }) => $alignItems || 'unset'};
`
W pierwszym kroku ustalamy, jakie i jakiego typu propsy będziemy przyjmować. Pozwoliłem sobie dać tu trochę więcej własności, niż te, których użyjemy teraz. Jednak z dużym prawdopodobieństwem będziemy używać ich w przyszłości, bo Wrapper w założeniu ma być mocno reużywalny. Lista też nie jest ostatecznie zamknięta - jeśli będzie potrzeba, możemy bez problemu dodać kolejne propsy w przyszłości.
Nie jest to miejsce, aby dokładnie wyjaśniać działanie TypeScriptu, dlatego powiem krótko, że deklarujemy, jakie własności możemy do komponentu przekazać oraz wskazujemy, że wszystkie one są opcjonalne.
Sam komponent jest bardzo prosty. Jeśli zrozumiemy działanie choć jednej linijki, to tę wiedzę możemy spokojnie przełożyć na wszystkie inne elementy. Weźmy zatem pierwszą:
position: ${({ $position }) => $position || 'static'};
Sprawdzamy, czy własność $position została przekazana. Jeśli tak, to do własności position
Wrappera przypisujemy wartość zmiennej. Jeśli zaś $position nie istnieje, wówczas używamy domyślnej wartości, czyli 'static'
.
Żeby zwiększyć nieco czytelność, możemy typy przenieść do interfejsu:
interface IWrapperProps {
$position?: 'static' | 'relative' | 'absolute' | 'sticky' | 'fixed'
$m?: string | number
$p?: string | number
$display?:
| 'block'
| 'inline'
| 'inline-block'
| 'flex'
| 'inline-flex'
| 'grid'
| 'inline-grid'
| 'flow-root'
$justifyContent?:
| 'center'
| 'start'
| 'end'
| 'flex-start'
| 'flex-end'
| 'left'
| 'right'
| 'space-between'
| 'space-around'
| 'space-evenly'
| 'stretch'
$alignItems?:
| 'center'
| 'start'
| 'end'
| 'flex-start'
| 'flex-end'
| 'self-start'
| 'self-end'
| 'anchor-center'
}
const StyledWrapper = styled.div<IWrapperProps>`
position: ${({ $position }) => $position || 'static'};
margin: ${({ $m }) => $m || 0};
padding: ${({ $p }) => $p || 0};
display: ${({ $display }) => $display || 'initial'};
justify-content: ${({ $justifyContent }) => $justifyContent || 'unset'};
align-items: ${({ $alignItems }) => $alignItems || 'unset'};
`
Pozostało nam w takim razie jedynie zaimplementować nasze style do Headera.
const Header = () => {
return (
<StyledHeader>
<StyledWrapper
$display="flex"
$alignItems="center"
$justifyContent="space-between"
$m="0 2rem"
>
<Image src={TempSwitcher} alt="" />
<StyledLangIcon />
</StyledWrapper>
<StyledWrapper $display="flex" $alignItems="center">
<StyledLogo src={Logo} alt="" />
<Title>Smoggy Foggy</Title>
</StyledWrapper>
</StyledHeader>
)
}
Możemy też ze StyledHeader usunąć stylowanie dla div
. Teraz nasz element nagłówka wygląda praktycznie tak, jak powinien.
Czas zająć się sprzątaniem.
Czyścimy kod
Jeżeli do tej pory, tak jak ja, pracowaliście tylko na pliku Header.tsx
, to najwyższa pora przenieść utworzone przy okazji pracy komponenty do osobnych plików.
Ponieważ większość komponentów już jest odseparowana od głównego kodu Headera to większość pracy mamy za sobą. Zatem po kolei.
W katalogu src/components/Atoms
tworzymy Logo/Logo.style.ts i wklejamy poniższy kod:
import styled from 'styled-components'
import Image from 'next/image'
const StyledLogo = styled(Image)`
height: 5rem;
width: 5rem;
margin-top: 6rem;
margin-right: 1.5rem;
`
export default StyledLogo
Dodatkowo można utworzyć osobny komponent dla całego logotypu. Zostajemy w katalogu src/components/Atoms/Logo i tworzymy plik Logo.tsx, który będzie zawierał:
import StyledLogo from '@/components/Atoms/Logo/Logo.style'
import LogoIcon from '@/assets/icons/logoLight.svg'
const Logo = () => {
return <StyledLogo src={LogoIcon} alt="" />
}
export default Logo
Dobrze, idziemy dalej.
Tworzymy, także w katalogu Atoms
, plik Title/Title.tsx
src/components/Atoms/Title/Title.style.ts
import styled from 'styled-components'
const StyledTitle = styled.h1`
font-family: ${({ theme }) => theme.fonts.header};
margin: 6rem 0 0;
font-size: 4rem;
`
export default StyledTitle
Następnie:
src/components/Atoms/Title/Title.tsx
import StyledTitle from '@/components/Atoms/Title/TItle.style'
const Title = () => {
return <StyledTitle>Smoggy Foggy</StyledTitle>
}
export default Title
Kolejnym atomem, jaki stworzymy będzie StyledLangIcon
src/components/Atoms/LangIcon/LangIcon.style.ts
import styled from 'styled-components'
import LangPicker from '@/assets/icons/langPicker.svg'
const StyledLangIcon = styled.div`
position: relative;
margin: 0.5rem;
border-radius: 50%;
background: url(${LangPicker.src}) no-repeat center/cover;
height: 3rem;
width: 3rem;
border: 0;
z-index: 2;
`
export default StyledLangIcon
Pojawia się pytanie, co z Wrapperem? Z jednej strony jest to... no właśnie, wrapper, czyli komponent zawierający inne komponenty. Z drugiej - de facto jest to bardzo prosty komponent, który można by uznać za atom.
Jeśli jednak weźmiemy pod uwagę założenia Atomic Design
These atoms include basic HTML elements like form labels, inputs, buttons, and others that can’t be broken down any further without ceasing to be functional.
Jest to jasna sugestia, że nasz Wrapper powinien być co najmniej molekułą - i tak go właśnie stworzymy.
src/components/Molecules/Wrapper/Wrapper.style.ts
interface IWrapperProps {
$position?: 'static' | 'relative' | 'absolute' | 'sticky' | 'fixed'
$m?: string | number
$p?: string | number
$display?:
| 'block'
| 'inline'
| 'inline-block'
| 'flex'
| 'inline-flex'
| 'grid'
| 'inline-grid'
| 'flow-root'
$justifyContent?:
| 'center'
| 'start'
| 'end'
| 'flex-start'
| 'flex-end'
| 'left'
| 'right'
| 'space-between'
| 'space-around'
| 'space-evenly'
| 'stretch'
$alignItems?:
| 'center'
| 'start'
| 'end'
| 'flex-start'
| 'flex-end'
| 'self-start'
| 'self-end'
| 'anchor-center'
}
const StyledWrapper = styled.div<IWrapperProps>`
position: ${({ $position }) => $position || 'static'};
margin: ${({ $m }) => $m || 0};
padding: ${({ $p }) => $p || 0};
display: ${({ $display }) => $display || 'initial'};
justify-content: ${({ $justifyContent }) => $justifyContent || 'unset'};
align-items: ${({ $alignItems }) => $alignItems || 'unset'};
`
export default StyledWrapper
src/components/Molecules/Wrapper/Wrapper.tsx
import { FC, ReactNode } from 'react'
import StyledWrapper, {
IStyledWrapperProps,
} from '@/components/Molecules/Wrapper/Wrapper.style'
interface IWrapperProps extends IStyledWrapperProps {
children: ReactNode
}
const Wrapper: FC<IWrapperProps> = ({ children, ...rest }) => {
// eslint-disable-next-line react/jsx-props-no-spreading
return <StyledWrapper {...rest}>{children}</StyledWrapper>
}
export default Wrapper
Kilka uwag do komponentu Wrapper.
Pojawia się tu kilka komplikacji. Z uwagi na to, że propsy przekazujemy do komponentu Wrapper tylko po to, żeby finalnie trafiły do StyledWrapper - chcemy sobie nieco ułatwić zadanie. Oczywiście, można byłoby przekazać własności po kolei, ale byłoby z tym trochę zachodu. Ponadto w chwili, kiedy dodawalibyśmy nowe propsy, to musielibyśmy pamiętać, żeby je zaktualizować w obu komponentach. Ale tak, można i wyglądałoby to tak:
import { FC, ReactNode } from 'react'
import StyledWrapper, {
IStyledWrapperProps,
} from '@/components/Molecules/Wrapper/Wrapper.style'
interface IWrapperProps extends IStyledWrapperProps {
children: ReactNode
}
const Wrapper: FC<IWrapperProps> = ({ children, position, m, p, display, justifyContent, alignItems }) => {
return
<StyledWrapper
$position={position}
$m={m}
$p={p}
$display={display
$justifyContent={justifyContent}
$alignItems={alignItems}
>
{children}
</StyledWrapper>
}
export default Wrapper
Jeśli chcecie - możecie zastosować to podejście. Ja jednak pozostanę przy przekazaniu propsów hurtowo przy pomocy spread oparatora.
Jeśli też pozostaniecie przy spreadzie, to najpewniej edytor pokaże wam błąd ESLint: Prop spreading is forbidden (react/ jsx-props-no-spreading)
. Cóż, ogólnie nie zaleca się używania spread operatora przy przekazywaniu propsów, ponieważ wówczas nie mamy pełnej kontroli nad tym, co przekazujemy do komponentu. Jednak, jeśli używać tego z głową - to nic nie powinno się złego wydarzyć.
Aby zatem pozbyć się błędu, musimy dać znać ESLintowi, że jesteśmy świadomi naszych działań i pomimo wszystko chcemy tak zrobić. Musimy zatem poprzedzić linijkę z naszym spreadem komentarzem o treści // eslint-disable-next-line react/jsx-props-no-spreading
. Alternatywnie, jeśli chcemy umożliwić sobie przekazywanie propsów za pomocą spreada w całej aplikacji, to w pliku .eslintrc.json szukamy sekcji rules
. Tam musimy dodać linijkę "react/jsx-props-no-spreading": "off",
. Ja pozostanę jednak przy wyłączeniu reguły miejscowo.
Ok. Mamy gotowy Wrapper, pozostało tylko przenieść style samego Headera do osobnego pliku oraz zaimportować nowoutworzone komponenty do Header.tsx.
Zaczniemy od utworzenia pliku src/components/Organisms/Header/Header.style.ts
import styled from 'styled-components'
const StyledHeader = styled.header`
width: 100%;
margin: 0 1rem 1rem;
`
export default StyledHeader
src/components/Organisms/Header/Header.tsx
'use client'
import Image from 'next/image'
import TempSwitcher from '@/assets/images/tempSwitcher.png'
import Logo from '@/components/Atoms/Logo/Logo'
import Title from '@/components/Atoms/Title/Title'
import StyledLangIcon from '@/components/Atoms/LangIcon/LangIcon.styls'
import Wrapper from '@/components/Molecules/Wrapper/Wrapper'
import StyledHeader from '@/components/Organisms/Header/Header.style'
const Header = () => {
return (
<StyledHeader>
<Wrapper
$display="flex"
$alignItems="center"
$justifyContent="space-between"
$m="0 2rem"
>
<Image src={TempSwitcher} alt="" />
<StyledLangIcon />
</Wrapper>
<Wrapper $display="flex" $alignItems="center">
<Logo />
<Title />
</Wrapper>
</StyledHeader>
)
}
export default Header
Gotowe! Udało nam się utworzyć pierwsze komponenty!
Jak zawsze wszystko można podejrzeć na branchu create-header.
Tradycyjnie już zapraszam na mojego twixera.
Do następnego!