Blog

Tworzymy globalne style

Budujemy aplikacjęProgramowanie

#aplikacja#projekt#kodowanie#style#style componentes#globalne style

Opublikowany 20.01.2025

Tworzymy globalne style

Najwyższy czas przejść do pisania prawdziwego kodu. Jednak zanim zaczniemy, to wypada przedstawić nieco teorii.

Jak ustaliliśmy, do stylowania naszej aplikacji użyjemy biblioteki Styled Components. Pora zatem wyjaśnić, czym ta biblioteka właściwie jest.

Stylowanie JavaScriptem

Styled Components jest przykładem tzw. CSSinJS. Ta nazwa oddaje idealnie, czym to jest - czyli pisanie kodu CSS za pomocą JavaScriptu (lub TypeScriptu, nie ma z tym najmniejszego problemu).

I można zadać słuszne pytanie - co nam to daje? Zwłaszcza, że współczesny CSS pozwala na naprawdę zaawansowane stylowanie, jakie jeszcze kilka lat temu nie byłoby możliwe bez dodatkowych bibliotek lub preprocesorów.

Otóż integracja kodu CSS w JS-ie daje nam ogromną zaletę, jaką jest pisanie kodu javascriptowego wewnątrz reguł CSS. Czy Styled Components to jedyna biblioteka pozwalająca na CSSinJS? Oczywiście, że nie. Czy CSSinJS pozawala na osiągnięcie efektów niedostępnych dla "klasycznego" stylowania? Też nie, jednak moim zdaniem umożliwia znaczne uproszczenie kodu - np. poprzez odejście od żonglowania klasami na rzecz operowania na przekazywanych do elementu zmiennych.

Nie chcę w tym miejscu nikogo przekonywać, że Styled Components (lub ogólnie CSSinJS) to jedyne słuszne podejście. W trakcje pracy nad aplikacją sami będziecie mieć możliwość zapoznania się w praktyce z zaletami i wadami tego rozwiązania. I sami wówczas zdecydujecie, czy takie podejście do pisania styli wam pasuje.

Globalne style w Styled Components

Podstawową funkcją Styled Components jest oczywiście możliwość stylowania poszczególnych elementów. Ale co w sytuacji, kiedy chcemy, żeby jakieś style były dostępne z poziomu całej aplikacji - np. domyślna czcionka, kolorystyka czy wielkość fontu? Jak najbardziej da się to zrobić - i nie jest to szczególnie skomplikowane. Z pomocą przychodzą globalne style.

W tym celu skorzystamy z wbudowanego w Styled Components komponentu CreateGlobalStyles. Zatem zabierzmy się za robotę!

Na samym początku warto utworzyć nowy branch (ja mój nazwałem add-global-styles) i poleceniami npm i styled-components oraz npm i -D @types/styled-components zainstalować naszą bibliotekę. Teraz tworzymy nowy plik. W założonym ostatnim razem katalogu lib możemy dodać podkatalog styles, a w nim plik GlobalStyle.ts. W nim najpierw importujemy wspomniane CreateGlobalStyles. Następnie tworzymy element GlobalStyles. Dalej już leci niemal klasyczny CSS. Zobaczcie:

import { createGlobalStyle } from 'styled-components'

const GlobalStyle = createGlobalStyle`
	*, *::after, *::before {
    box-sizing: border-box;
  }

  html {
    font-size: 62.5%;
  }

  body {
    margin: 0;
    padding: 0;
    line-height: 2;
  }

  h1, h2, h3, h4, h5, h6 {
    margin: 0;
    line-height: 1.5;
  }
`

export default GlobalStyle

Mamy w ten sposób utworzony pewien bardzo podstawowy styl globalny. Jak widać doda on pewne właściwości CSS do naszego dokumentu - konkretnie do wszystkich pseudoelementów ::before i ::after, do html, body i nagłówków. Oczywiście nic nie stoi na przeszkodzie, alby w tym miejscu dodać domyślne stylowanie także dla innych tagów HTML.

Zanim jednak przejdziemy do właściwego stylowania zgodnego z przygotowanym designem warto pochylić się nad jedną kwestią. Wspomniałem, że globalne style dają nam możliwość ustawienia domyślnych fontów, kolorów itp. I moglibyśmy naturalnie wklepać wszystko "na sztywno". Jest jednak lepsza metoda. Metoda, która zdecydowanie ułatwi nam pracę w przyszłości i zadba, byśmy nie popełnili jakiejś głupiej literówki. Konkretnie utworzymy sobie obiekt, gdzie będziemy przechowywać nasze kolory itp.

W tym celu skorzystamy z wbudowanego w bibliotekę Styled Componens providera, czyli komponentu oplatającego naszą aplikację, który przekazuje swoje właściwości do wszystkich elementów potomnych (w dużym skrócie). Z providerami przy budowie naszej aplikacji spotkamy się jeszcze nie raz, ale na razie skupimy się na naszych stylach globalnych.

Obiekt theme

Wspomniany provider przyjmuje własność theme, która to właśnie będzie przekazywana dalej. Utwórzmy zatem obiekt, który przekażemy do providera.

W katalogu lib/styles tworzymy plik theme.ts, gdzie zdefiniujemy interesujące nas właściwości.

const theme = {
  colors: {
    darkRed: '#A9001F',
    lightRed: '#EE042E',
    grey: '#C4C4C4',
    white: '#F3F3F3',
    black: '#131313',
    darkGreen: '#2E9809',
    lightGreen: '#71EF45',
    steelBlue: '#21424D',
    darkBlue: '#56AAC5',
    lightBlue: '#A8E2F4',
  },
}

export default theme

Ok, mamy zdefiniowane kolory (dobrane na podstawie projektu), które będą używane w aplikacji. Czas na fonty. Wesprzemy się biblioteką font dostępną natywnie w NextJS.

Pozostając w katalogu lib/styles tworzymy plik fonts.ts

// eslint-disable-next-line camelcase
import { Montserrat, Cabin_Sketch } from 'next/font/google'

export const montserrat = Montserrat({
  weight: ['400', '700', '900'],
  style: 'normal',
  subsets: ['latin'],
})

export const cabin = Cabin_Sketch({
  weight: '700',
  style: 'normal',
  subsets: ['latin'],
})

Dobrze, zadeklarowaliśmy fonty, których użyjemy w naszej aplikacji. Dodajmy je do obiektu theme

import { montserrat, cabin } from '@/utils/lib/styles/fonts'

const theme = {
  colors: {
    darkRed: '#A9001F',
    lightRed: '#EE042E',
    grey: '#C4C4C4',
    white: '#F3F3F3',
    black: '#131313',
    darkGreen: '#2E9809',
    lightGreen: '#71EF45',
    steelBlue: '#21424D',
    darkBlue: '#56AAC5',
    lightBlue: '#A8E2F4',
  },

  fonts: {
    main: montserrat.style.fontFamily,
    header: cabin.style.fontFamily,
  },

  fontSize: {
    m: '1.6rem',
    l: '2rem',
    xl: '4rem',
  },

  fontWeights: {
    regular: '400',
    semiBold: '700',
    bold: '900',
  },
}

export default theme

Chętni mogą podzielić obiekt na mniejsze (np. colors, fonts, fontWeights), przenieść je do osobnych plików i w tym miejscu jedynie importować poszczególne obiekty z konkretnymi własnościami.

Dobrze, do theme jeszcze wrócimy, ale póki co zróbmy użytek z tego, co już mamy. Czas dodać provider!

Tworzymy nasz pierwszy provider

Można teraz przejść do pliku src/app/layout.tsx. Tam zaczynamy od zaimportowania providera z biblioteki, następnie oplatamy komponentem ThemeProvider całą aplikację.

import type { Metadata } from 'next'
import { ReactNode } from 'react'
import { ThemeProvider } from 'styled-components'
import theme from '@/utils/lib/styles/theme'

export const metadata: Metadata = {
  title: 'Smoggy Foggy',
  description: 'Sprawdź jakość powietrza',
}

const RootLayout = ({
  children,
}: Readonly<{
  children: ReactNode
}>) => {
  return (
    <ThemeProvider theme={theme}>
      <html lang="en">
        <body>{children}</body>
      </html>
    </ThemeProvider>
  )
}

export default RootLayout

Zaczynamy używać theme

Dobrze, wróćmy teraz do naszego globalnego stylu (dla przypomnienia lib/styles/GlobalStyle.ts). Na tym etapie możemy zacząć definiować własności za pomocą zmiennych zdefiniowanych w pliku theme.ts

import { createGlobalStyle } from 'styled-components'

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 }: ColorProps) =>
      $isDark ? theme.colors.white : theme.colors.black};
  }

  h1, h2, h3, h4, h5, h6 {
    margin: 0;
    line-height: 1.5;
  }
`

export default GlobalStyle

Jak pewnie zauważyliście, we własności color pojawiła się zmienna $isDark. Będzie ona używana do obsługi zmiany pomiędzy stylem jasnym i ciemnym całej aplikacji.

Ostatnia prosta

W tym miejscu nie pozostało nam nic innego, jak włączyć globalny styl w naszej aplikacji. W tym celu wracamy do pliku layout (src/app/layout.tsx) i pod providerem dodajemy utworzony GlobalStyle.

import type { Metadata } from 'next'
import { ReactNode } from 'react'
import { ThemeProvider } from 'styled-components'
import theme from '@/utils/lib/styles/theme'
import GlobalStyle from '@/utils/lib/styles/GlobalStyle'

export const metadata: Metadata = {
  title: 'Smoggy Foggy',
  description: 'Sprawdź jakość powietrza',
}

const RootLayout = ({
  children,
}: Readonly<{
  children: ReactNode
}>) => {
  return (
    <ThemeProvider theme={theme}>
      <GlobalStyle />
      <html lang="en">
        <body>{children}</body>
      </html>
    </ThemeProvider>
  )
}

export default RootLayout

I to wszystko. Teraz jeszcze dla pewności sprawdźmy, czy nasze style i provider działają.

Here we go again

Odpalamy środowisko developerskie (npm run dev) i... oczywiście nie działa. W przeglądarce widzimy poniższy błąd:

Error: createContext only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/context-in-server-component

Mówi on nam o tym, że element wykorzystujący createContext musi być komponentem działającym po stronie klienta (Client Component). Tylko... że nie mamy nigdzie kontekstu. Prawda? No nie do końca. Tak naprawdę nasz provider wykorzystuje właśnie kontekst, którym jest własność theme. Czyli co? Wystarczy, że - zgodnie z podpowiedzią - użyjemy use client w komponencie z kontekstem i będzie ok? No... zresztą sprawdźcie sami. Modyfikujemy nasz layout

'use client'

import type { Metadata } from 'next'
import { ReactNode } from 'react'
import { ThemeProvider } from 'styled-components'
import theme from '@/utils/lib/styles/theme'
import GlobalStyle from '@/utils/lib/styles/GlobalStyle'

export const metadata: Metadata = {
  title: 'Smoggy Foggy',
  description: 'Sprawdź jakość powietrza',
}

const RootLayout = ({
  children,
}: Readonly<{
  children: ReactNode
}>) => {
  return (
    <ThemeProvider theme={theme}>
      <GlobalStyle />
      <html lang="en">
        <body>{children}</body>
      </html>
    </ThemeProvider>
  )
}

export default RootLayout

W tym miejscu już edytor, w którym pracujemy powinien nam dać znać, że coś jest nie tak. Jeśli jednak tak się nie stało, to i tak w przeglądarce zobaczymy nowy błąd:

Error:
  × You are attempting to export "metadata" from a component marked with "use client", which is disallowed. Either remove the export, or the "use client" directive. Read more: https://nextjs.org/
  │ docs/getting-started/react-essentials#the-use-client-directive
  │
  │
    ╭─[/home/gacek/Projects/smoggy_foggy_2.0/src/app/layout.tsx:6:1]
  6 │ import theme from '@/utils/lib/styles/theme'
  7 │ import GlobalStyle from '@/utils/lib/styles/GlobalStyle'
  8 │
  9 │ export const metadata: Metadata = {
    ·              ────────
 10 │   title: 'Smoggy Foggy',
 11 │   description: 'Sprawdź jakość powietrza',
 12 │ }
    ╰────

Czyli nie możemy użyć use client w komponencie layout, ponieważ koliduje to z metadata. Jeśli waszą pierwszą myślą było usunięcie wspomnianego metadata, to wstrzymajcie konie. Istnieje bardziej eleganckie rozwiązanie.

Providery wszystkich krajów - łączcie się!

Przejdźmy do katalogu lib i stwórzmy tam kolejny podkatalog providers. W nim zaś utwórzmy plik AppProviders.tsx, w którym będziemy trzymać wszystkie providery, jakich użyjemy w aplikacji. Póki co wygląda on następująco:

'use client'

import { ReactNode } from 'react'
import { ThemeProvider } from 'styled-components'
import theme from '@/utils/lib/styles/theme'
import GlobalStyle from '@/utils/lib/styles/GlobalStyle'

const AppProviders = ({ children }: { children: ReactNode }) => (
  <ThemeProvider theme={theme}>
    <GlobalStyle />
    {children}
  </ThemeProvider>
)

export default AppProviders

Jak widać, udało się nam dać wymagany use client. Nie pozostało nam zatem nic innego, jak zamienić nasze providery w layoucie (src/app/layout.tsx) na zbiorczy AppProvider.

Ważne - jeśli nie dodamy naszego ulubionego use client także w pliku src/app/page.tsx, to błąd będzie nas nadal prześladował!

import type { Metadata } from 'next'
import { ReactNode } from 'react'
import AppProviders from '@/utils/lib/providers/AppProviders'

export const metadata: Metadata = {
  title: 'Smoggy Foggy',
  description: 'Sprawdź jakość powietrza',
}

const RootLayout = ({
  children,
}: Readonly<{
  children: ReactNode
}>) => {
  return (
    <AppProviders>
      <html lang="en">
        <body>{children}</body>
      </html>
    </AppProviders>
  )
}

export default RootLayout

Wprowadźmy teraz jakieś zmiany w pliku page.tsx, żeby zobaczyć, czy nasze style działają.

'use client'

import styled from 'styled-components'

const StyledHome = styled.main`
  display: flex;
  justify-content: center;
  align-items: center;

  p {
    color: ${({ theme }) => theme.colors.darkRed};
    font-weight: ${({ theme }) => theme.fontWeight.bold};
  }
`

const Home = () => {
  return (
    <StyledHome>
      Lorem Ipsum <p>Dolor sit amet</p>
    </StyledHome>
  )
}

export default Home

I voilà!

widok aplikacji

Słowo na koniec

Jeśli chcecie zobaczyć, jak u mnie wygląda aplikacja, to zapraszam na branch add-global-styles.

Tymczasem, jak zawsze, zapraszam na mojego twixera 😊

Tyle ode mnie, do następnego!