Jak pisać lepszy kod w React
Używam Reacta od ponad roku. Nie nazwałbym się ekspertem, ale stworzyłem kilka projektów, które pomogły mi lepiej zrozumieć tę bibliotekę i dostrzec kilka sztuczek oraz wzorców, które sprawiają, że mój kod jest bardziej wydajny. W tym artykule chciałbym się podzielić kilkoma wskazówkami i sztuczkami, które rzeczywiście poprawią Twój kod Reacta.
Destrukturyzacja props
Destrukturyzacja obiektów (zwłaszcza props, czyli właściwości) w JavaScript może znacznie uprościć kod. Spójrz na poniższy przykład.
import React from 'react';
import CoffeeCard from './CoffeeCard';
const CafeMenu = () => {
const coffeeList = [
{
id: '0',
name: 'Espresso',
price: '2.00',
size: '16'
},
{
id: '1',
name: 'Cappuccino',
price: '3.50',
size: '24'
},
{
id: '2',
name: 'Caffee Latte',
price: '2.70',
size: '12'
}
];
return coffeeList.map(item => (
<CoffeeCard key={item.id} coffee={item} />
));
};
export default CafeMenu;
Komponent nadrzędny
Mamy komponent CoffeMenu, który przechowuje listę dostępnych napojów. Chcemy teraz stworzyć kolejny komponent, który będzie mógł wyświetlać tylko jeden napój. Bez destrukturyzacji właściwości, nasz kod będzie wyglądał następująco:
import React from 'react';
const CoffeeCard = props => {
return (
<div>
<h1>{props.coffee.name}</h1>
<p>Price: {props.coffee.price}$</p>
<p>Size: {props.coffee.size} oz</p>
</div>
);
};
export default CoffeeCard;
Komponent potomny (bez destrukturyzacji)
Jak widać, nie wygląda to dobrze. Musimy powtarzać „props.coffee” za każdym razem, gdy chcemy pobrać wartość tej właściwości. Na szczęście istnieje na to inny (czystszy) sposób.
import React from 'react';
const CoffeeCard = props => {
const { name, price, size } = props.coffee;
return (
<div>
<h1>{name}</h1>
<p>Price: {price}$</p>
<p>Size: {size} oz</p>
</div>
);
};
export default CoffeeCard;
Komponent potomny (po destrukturyzacji)
Jeśli chcemy przekazać wiele parametrów do komponentu potomnego, to możemy również destrukturyzować właściwości bezpośrednio w konstruktorze (lub parametrach komponentu funkcyjnego). Oto przykład:
import React from 'react';
import ContactInfo from './ContactInfo';
const UserProfile = () => {
const name = 'John Locke';
const email = '[email protected]';
const phone = '01632 960668';
return <ContactInfo name={name} email={email} phone={phone} />;
};
export default UserProfile;
Komponent nadrzędny
import React from 'react';
const ContactInfo = ({ name, email, phone }) => {
return (
<div>
<h1>{name}</h1>
<p> E-mail: {email}</p>
<p> Phone: {phone}</p>
</div>
);
};
export default ContactInfo;
Komponent potomny
Kolejność importowania modułów
Czasami (szczególnie w Container Components - spójrz na punkt 4) musimy użyć wielu różnych modułów, a importowane przez nas komponenty wyglądają trochę niechlujnie.
import { Auth } from 'aws-amplify';
import React from 'react';
import SidebarNavigation from './components/SidebarNavigation';
import { EuiPage, EuiPageBody } from '@elastic/eui';
import { keyCodes } from '@elastic/eui/lib/services';
import './index.css'
import HeaderNavigation from './components/HeaderNavigation';
import Routes from './Routes';
Istnieje wiele różnych poglądów na temat idealnej kolejności importowania modułów. Polecam się rozejrzeć i używać takiego, który działa najlepiej dla Ciebie. Jeśli chodzi o mnie, zwykle grupuję importy według typu i sortuję je alfabetycznie (to jest akurat opcjonalne). Staram się też zachować następującą kolejność:
- Standardowe moduły
- Zewnętrzne moduły
- Zaimportowany kod (komponenty itd.)
- Zaimportowane elementy specyficzne dla modułu (np. CSS, PNG itd.)
- Kod używany tylko przy testach
Po szybkiej refaktoryzacji nasze importy zaczynają wyglądać o wiele lepiej.
import React from 'react';
import { Auth } from 'aws-amplify';
import { EuiPage, EuiPageBody } from '@elastic/eui';
import { keyCodes } from '@elastic/eui/lib/services';
import HeaderNavigation from './components/HeaderNavigation';
import SidebarNavigation from './components/SidebarNavigation';
import Routes from './Routes';
import './index.css'
Używaj fragmentów
W naszych komponentach często zwracamy wiele elementów. Pojedynczy komponent React nie może zwrócić wielu elementów potomnych, zatem zwykle umieszczamy je w div
. Takie rozwiązanie może być czasem naprawdę problematyczne. Spójrzmy na poniższy przykład.
Chcemy utworzyć komponent Table
, który zawiera komponent o nazwie Columns
.
import React from 'react';
import Columns from './Columns';
const Table = () => {
return (
<table>
<tbody>
<tr>
<Columns />
</tr>
</tbody>
</table>
);
};
export default Table;
Komponent Columns zawiera kilka elementów td
. Ponieważ nie możemy zwrócić wielu wartości, musimy umieścić te elementy w div
.
import React from 'react';
const Columns = () => {
return (
<div>
<td>Hello</td>
<td>World</td>
</div>
);
};
export default Columns;
W rezultacie dostajemy błąd, ponieważ nie wolno nam umieszczać elementu div
w tagu tr
. Rozwiązaniem jest użycie tagu Fragment
tak jak poniżej:
import React, { Fragment } from 'react';
const Columns = () => {
return (
<Fragment>
<td>Hello</td>
<td>World</td>
</Fragment>
);
};
export default Columns;
Możemy traktować Fragment
jako niewidoczny element div
. Opakowuje on elementy w komponencie potomnym, przenosi je do komponentu nadrzędnego i znika. Możesz też użyć krótszej składni, ale nie obsługuje ona kluczy i atrybutów.
import React from 'react';
const Columns = () => {
return (
<>
<td>Hello</td>
<td>World</td>
</>
);
};
export default Columns;
Używaj komponentów prezentacyjnych i kontenerowych
Dobrym pomysłem jest podzielenie składników aplikacji na komponenty prezentacyjne (głupie) i kontenerowe (inteligentne). Jeśli nie wiesz, co to jest, spójrz na poniższą charakterystykę:
Komponenty prezentacyjne
- Skupiają się na UI. Odpowiadają za wygląd komponentów
- Wszystkie dane są dostarczane przez właściwości. Głupie komponenty nie powinny wykonywać wywołań API. To zadanie dla inteligentnych komponentów
- Nie wymagają innych zależności aplikacji niż pakiety UI
- Mogą zawierać stan, ale tylko do manipulowania UI — nie powinny przechowywać danych aplikacji.
Przykłady głupich komponentów: loadery, modale, przyciski, pola formularza.
Komponenty kontenerowe
- Zazwyczaj nie obejmują stylizacji
- Służą do manipulowania danymi. Mogą pobierać, rejestrować zmiany i przekazywać dane aplikacji
- Są odpowiedzialne za zarządzanie stanem, ponowne renderowanie komponentów itp.
- Mogą wymagać zależności aplikacji, wywołania Redux, metod cyklu życia, API, bibliotek itp.
Korzyści z używania komponentów prezentacyjnych i kontenerowych
- Zwiększenie czytelności
- Łatwiejsze ponowne użycie kodu
- Lepsza testowalność
Co więcej, są one zgodne z zasadą jednej odpowiedzialności (ang. Single Responsibility Principle) - jeden komponent odpowiada za wygląd, a drugi za dane.
Przykład
Spójrzmy na prosty przykład. Oto komponent BookList, który pobiera dane książek z API i wyświetla je na liście.
import React, { useState, useEffect } from 'react';
const BookList = () => {
const [books, setBooks] = useState([]);
const [isLoading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
fetch('api/books')
.then(res => res.json())
.then(books => {
setBooks(books);
setLoading(false);
});
}, []);
const renderLoading = () => {
return <p>Loading...</p>;
};
const renderBooks = () => {
return (
<ul>
{books.map(book => (
<li>{book.name}</li>
))}
</ul>
);
};
return <>{isLoading ? renderLoading() : renderBooks()}</>;
};
export default BookList;
Problem z takim komponentem polega na tym, że jest on odpowiedzialny za zbyt wiele rzeczy. Pobiera i renderuje dane. Jest on również powiązany z jednym konkretnym punktem końcowym, więc nie możesz używać tego komponentu do wyświetlania np. listy książek określonego użytkownika bez duplikowania kodu. Spróbujmy podzielić ten komponent na komponenty prezentacyjne i kontenerowe.
import React from 'react';
const BookList = ({ books, isLoading }) => {
const renderLoading = () => {
return <p>Loading...</p>;
};
const renderBooks = () => {
return (
<ul>
{books.map(book => (
<li key={book.id}>{book.name}</li>
))}
</ul>
);
};
return <>{isLoading ? renderLoading() : renderBooks()}</>;
};
export default BookList;
Komponent prezentacyjny listy książek
import React, { useState, useEffect } from 'react';
import BookList from './BookList';
const BookListContainer = () => {
const [books, setBooks] = useState([]);
const [isLoading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
fetch('/api/books')
.then(res => res.json())
.then(books => {
setBooks(books);
setLoading(false);
});
}, []);
return <BookList books={books} isLoading={isLoading} />;
};
export default BookListContainer;
Komponent kontenerowy listy książek
Jak widać, wygląda to znacznie lepiej. Co więcej, pozwala nam używać komponentu BookList
w wielu miejscach z różnymi danymi.
Używaj styled-components
Stylizacja komponentów Reacta zawsze była dość problematyczna. Znajdowanie błędnych nazw klas, utrzymywanie dużych plików CSS, obsługa problemów ze zgodnością może czasem boleć. Styled components umożliwiają pisanie CSS w JavaScript przy użyciu szablonów. Dzięki nim możesz dynamicznie stylizować komponenty, nie używać nazw klas i zoptymalizować CSS.
Aby zacząć, musisz dodać bibliotekę styled-components do swojego projektu React za pomocą NPM:
npm i styled-components
Stwórzmy teraz nasz pierwszy stylizowany komponent.
import React from 'react';
import styled from 'styled-components';
const Grid = styled.div`
display: flex;
`;
const Col = styled.div`
display: flex;
flex-direction: column;
`;
const MySCButton = styled.button`
background: ${props => (props.primary ? props.mainColor : 'white')};
color: ${props => (props.primary ? 'white' : props.mainColor)};
display: block;
font-size: 1em;
margin: 1em;
padding: 0.5em 1em;
border: 2px solid ${props => props.mainColor};
border-radius: 15px;
`;
function App() {
return (
<Grid>
<Col>
<MySCButton mainColor='#ee6352' primary>My 1st Button</MySCButton>
<MySCButton mainColor='#ee6352'>My 2st Button</MySCButton>
<MySCButton mainColor='#ee6352'>My 3st Button</MySCButton>
</Col>
<Col>
<MySCButton mainColor='#515052' primary>My 4st Button</MySCButton>
<MySCButton mainColor='#515052'>My 5st Button</MySCButton>
<MySCButton mainColor='#515052'>My 6st Button</MySCButton>
</Col>
</Grid>
);
}
export default App;
Przyciski i grid przy użyciu stylizowanych komponentów Wynik możecie zobaczyć tutaj. To był tylko prosty przykład działania takich komponentów. Ogólnie stać je na wiele więcej. Możesz przeczytać więcej o styled components w ich oficjalnej dokumentacji.
Dziękuje za uwagę! Mam nadzieję, że nauczyliście się czegoś nowego.
Oryginał tekstu w języku angielskim przeczytasz tutaj.