W ten wspaniały poniedziałek otwieram serię postów dotyczącą nauki React’a na autentycznych przykładach. Naszym wspólnym zadaniem będzie odwzorowanie komponentów, które znajdziemy w sieci, omawiając przy tym ciekawsze tematy z kategorii React.js. W najbliższym czasie dowiemy się wiele o styled-components, Pure Components oraz Higher Order Components, więc jest na co czekać. Kolejne posty pojawią się w kategorii react-essentials.

Przejdźmy zatem do meritum. Dzisiaj na warsztat trafia npm package o nazwie styled-components, w którym zakochałem się (platonicznie) kilka tygodni temu, gdy odkryłem, że te narzędzie o niepozornym logo jest potężniejsze niż jakikolwiek CSS’owy preprocesor.

Wyobraź sobie, drogi czytelniku, że nagle Twój kod CSS uzbraja się w składnię i możliwości EcmaScript2015. Od teraz łatwo i szybko piszesz nowe funkcje operujące na stylach, możesz korelować style z Reduxowym storem, Reactowym statem lub jakimi tylko chcesz danymi. Ponadto nie potrzebujesz osobnych plików ze stylami, więc komponenty są teraz absolutnie autonomiczne, a stylowanie odbywa się na najniższym poziomie – czy może być coś piękniejszego i prostszego?

Zobaczmy zatem, co dziś zakodujemy (live demo):

react component screen

Wybrałem komponent renderujący pytania na StackOverflow, ponieważ nie jest za trudny na początek, a mimo to pozwoli pokazać później ciekawe zagadnienia dotyczące optymalizacji i nadkomponentów (nazwa własna). Środowisko jest praktycznie dowolne, jednak ze swojej strony polecam przejść z webpacka na create-react-app, ponieważ jest stworzone bezpośrednio przez ekipę od Reacta, a któż inny mógłby to zrobić lepiej. Oprócz tego potrzebny nam będzie jedynie pakiet styled-components, który instalujemy przez npm.

Zaczynamy od standardowego snippeta na nowy komponent, importując od razu funkcję styled:

import React, { Component } from 'react';
import styled from 'styled-components';

class QuestionComponent extends Component {
  render() {
    return(
      <div>Hello World</div>
    );
  }
}

export default QuestionComponent;

Funkcja styled wykorzystuje rzadko spotykaną składnię z ES6, czyli tagged template literals i prezentuje się w ten sposób:

const Container = styled.div`
  margin: 50px auto;
  background: #fdfdfd;
  width: 721px;
  min-height: 200px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
  display: flex;
`;

Jak widać, Container to jest ostylowany element HTML, który jednocześnie posiada właściwości react’owego komponentu, o czym zaraz. Definicję naszego Containera umieszczamy poza klasą głównego komponentu i korzystamy nie inaczej niż zwykle:

  render() {
    return(
      <Container>Hello World</Container>
    );
  }

Kolejnym krokiem jest rozbicie komponentu na mniejsze, w naszym przypadku można zrobić to w następujący sposób:

  • Votes
    • VoteUp
    • Score
    • VoteDown
    • Favorite
  • Content
    • Question
    • Tags

Do stworzenia mamy zatem jeszcze kilka komponentów, jednak 2 z nich to tylko Containery, a VoteUp i VoteDown to tak naprawdę ten sam komponent tylko, że odwrócony. Skorzystamy, więc z możliwości styled-components i na podstawie podanych props wyrenderujemy różne style:

const VoteButton = styled.button`
  background: ${props => props.active ? cls.btn.active.vote : cls.btn.vote };
  height: 15px;
  width: 30px;
  border: none;
  clip-path: ${props => props.down ? 'polygon(50% 100%, 0 0, 100% 0)' : 'polygon(50% 0%, 0% 100%, 100% 100%)'};
  &:hover { cursor: pointer; }
`;

Teraz wystarczy tylko dopisać down jako właściwość komponentu i voila, mamy drugi przycisk odwrócony:

<VoteButton down />

Przygotowałem również styl przycisku w przypadku, gdy jest aktywny, te cls.btn.active.vote etc. to są kolory, które zebrałem ładnie w jednym obiekcie – prosto i intuicyjnie:

const cls = {
  btn: {
    vote: '#858C93',
    star: '#C9CBCF',
    active: {
      vote: '#F48024',
      star: '#FFD83D'
    }
  },
  tags: {
    background: '#E1ECF4',
    text: '#39739d',
    hover: '#cee0ed'
  }
};

Aktualny kod zawiera na razie same style (dodałem również proste elementy nie wymagające komentarza) i jest odarty z logiki i treści:

import React, { Component } from 'react';
import styled from 'styled-components';

class QuestionComponent extends Component {
  render()  {
    return(
      <Container>
        <Votes>
          <VoteButton />
          <Score>0</Score>
          <VoteButton down />
        </Votes>
        <Content>

        </Content>
      </Container>
    );
  }
}

export default QuestionComponent;

const cls = {
  btn: {
    vote: '#858C93',
    star: '#C9CBCF',
    active: {
      vote: '#F48024',
      star: '#FFD83D'
    }
  },
  tags: {
    background: '#E1ECF4',
    text: '#39739d',
    hover: '#cee0ed'
  }
};

const Container = styled.div`
  margin: 50px auto;
  background: #fdfdfd;
  width: 721px;
  min-height: 200px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
  display: flex;
`;

const Votes = styled.div`
  padding: 10px;
  margin: 5px;
  display: flex;
  flex-direction: column;
  align-items: center;
`;

const VoteButton = styled.button`
  background: ${props => props.active ? cls.btn.active.vote : cls.btn.vote };
  height: 15px;
  width: 30px;
  border: none;
  clip-path: ${props => props.down ? 'polygon(50% 100%, 0 0, 100% 0)' : 'polygon(50% 0%, 0% 100%, 100% 100%)'};
  &:hover { cursor: pointer; }
`;

const Score = styled.span`
  margin: 12px 0;
`;

const Content = styled.span`
  text-align: left;
  padding: 15px 15px 0 10px;
`;

Dodajmy zatem jakieś dummy data, które normalnie otrzymalibyśmy w GET’cie, ale dla wygody wrzucimy je do this.state:

  constructor(props) {
    super(props);
    this.state = { //dummyData
      votes: 4, //suma wszystkich głosów
      indVote: 0, //indywidualny głos użytkownika, czyli 1, 0 lub -1
      favorite: false,
      questionContent: 'I am writing a nodejs application that I would like to use as both a web application, as well as an API provider. Once a user is authenticated, I want to assign that user a token to be used for subsequent requests. This works great with passport for the web application, as I just serialize and deserialize the user with the token in the session. However, when responding to API requests, there is no cookie to set to store the session information. Ideally, passport would look for the token both in session and the request body. Is there any way to configure passport to accomplish this?',
      tags: ['node.js', 'express.js', 'passport.js']
    };
  }

Teraz możemy napisać funkcję handleVoteEvent, która uwzględnia nasz głos i odwzorowuje zachowanie licznika ze StackOverflow, co muszę przyznać nie jest takie proste jak się wydaje, jednak nie ma tu nic niezrozumiałego – kwestia dodawania i odejmowania głosów, więc również pozostawię bez zbędnego komentarza:

  handleVoteEvent(vote) {
    const { indVote, votes } = this.state;
    if(indVote !== 0) {
      if(indVote === vote) {
        this.setState({ indVote: 0, votes: votes-vote });
      } else {
        this.setState({ indVote: -indVote, votes: votes+vote-indVote });
      }
    } else {
      this.setState({ indVote: vote, votes: votes+vote });
    }
  }

Następnym krokiem jest jej implementacja, warto zwrócić uwagę na to jak przypisać przyciskom różne głosy (1 lub -1), czyli .bind(this, -1). Dodałem od razu właściwości active, zauważcie, jak proste i logiczne jest te rozwiązanie:

  render()  {
    return(
      <Container>
        <Votes>
          <VoteButton active={this.state.indVote > 0} onClick={this.handleVoteEvent.bind(this, 1)}/>
          <Score>{this.state.votes}</Score>
          <VoteButton down active={this.state.indVote < 0} onClick={this.handleVoteEvent.bind(this, -1)} />
        </Votes>
        <Content>
         {this.state.questionContent}
        </Content>
      </Container>
    );
  }

Dzięki styled-components nasz komponent wygląda schludnie i czysto, a na dzisiaj to już praktycznie koniec moich spostrzeżeń na temat kodu, ponieważ mam dla Was, chcących sprawdzić siebie, małe zadanko domowe, aby w podobny sposób zaimplementować gwiazdkę dodającą pytanie do ulubionych i tagi ze StackOverflow, tym samym dokańczając komponent (pełna wersja i tak znajduje się na wspomnianym demo, więc można tam porównać rozwiązanie). Poza tym, wierzę, że posmakowaliście stylowania za pomocą styled-components i zgłębicie temat, ponieważ teraz poznaliśmy jedynie podstawowe możliwości – nie wszystko na raz 🙂 Dopiero w przyszłości ogarnę na blogu bardziej zaawansowane funkcje takie jak np. theming. Do następnego!