Praca z hookami React i TypeScript
Opublikowany: 2022-03-11Hooki zostały wprowadzone do Reacta w lutym 2019 roku, aby poprawić czytelność kodu. O hookach Reacta mówiliśmy już w poprzednich artykułach, ale tym razem sprawdzamy, jak hooki współpracują z TypeScript.
Przed hakami komponenty React miały dwa smaki:
- Klasy obsługujące stan
- Funkcje , które są w pełni zdefiniowane przez ich rekwizyty
Naturalnym ich zastosowaniem było budowanie złożonych komponentów kontenerowych z klasami i prostych komponentów prezentacyjnych z czystymi funkcjami.
Czym są haki reagujące?
Komponenty kontenera obsługują zarządzanie stanem i żądania do serwera, które będą następnie wywołane w tym artykule efektami ubocznymi . Stan będzie propagowany do elementów potomnych kontenera za pośrednictwem rekwizytów.
Jednak wraz z rozwojem kodu komponenty funkcjonalne mają tendencję do przekształcania się w komponenty kontenera.
Aktualizacja funkcjonalnego komponentu do bardziej inteligentnego nie jest aż tak bolesna, ale jest to czasochłonne i nieprzyjemne zadanie. Co więcej, bardzo rygorystyczne rozróżnianie prezenterów i kontenerów nie jest już mile widziane.
Hooki mogą robić jedno i drugie, więc wynikowy kod jest bardziej jednolity i ma prawie wszystkie zalety. Oto przykład dodawania stanu lokalnego do małego komponentu, który obsługuje sygnaturę oferty.
// put signature in local state and toggle signature when signed changes function QuotationSignature({quotation}) { const [signed, setSigned] = useState(quotation.signed); useEffect(() => { fetchPost(`quotation/${quotation.number}/sign`) }, [signed]); // effect will be fired when signed changes return <> <input type="checkbox" checked={signed} onChange={() => {setSigned(!signed)}}/> Signature </> }
Jest to duża zaleta – kodowanie za pomocą TypeScript było świetne w Angular, ale nadęte w React. Jednak kodowanie haków React za pomocą TypeScript jest przyjemnym doświadczeniem.
TypeScript ze starym React
TypeScript został zaprojektowany przez Microsoft i podążał ścieżką Angular, gdy React opracował Flow , który teraz traci przyczepność. Pisanie klas Reacta z naiwnym TypeScriptem było dość bolesne, ponieważ programiści Reacta musieli wpisywać zarówno props
, jak i state
, mimo że wiele kluczy było takich samych.
Oto prosty obiekt domeny. Tworzymy aplikację cytatową typu Quotation
, zarządzaną w niektórych komponentach crud ze stanem i propsami. Quotation
może zostać utworzona, a jej status może zmienić się na podpisany lub nie.
interface Quotation{ id: number title:string; lines:QuotationLine[] price: number } interface QuotationState{ readonly quotation:Quotation; signed: boolean } interface QuotationProps{ quotation:Quotation; } class QuotationPage extends Component<QuotationProps, QuotationState> { // ... }
Ale wyobraź sobie, że strona QuotationPage poprosi teraz serwer o identyfikator : na przykład 678. wycenę dokonaną przez firmę. Cóż, oznacza to, że cytatProps nie zna tej ważnej liczby — nie jest to dokładne opakowanie cytatu . Musimy zadeklarować znacznie więcej kodu w interfejsie QuotationProps:
interface QuotationProps{ // ... all the attributes of Quotation but id title:string; lines:QuotationLine[] price: number }
Kopiujemy wszystkie atrybuty oprócz id w nowym typie. Hm. To przywodzi mi na myśl starą Javę, która wymagała pisania kilku DTO. Aby to przezwyciężyć, zwiększymy naszą wiedzę na temat TypeScript, aby ominąć ból.
Zalety TypeScriptu z hookami
Korzystając z hooków, będziemy mogli pozbyć się poprzedniego interfejsu QuotationState. W tym celu podzielimy QuotationState na dwie różne części stanu.
interface QuotationProps{ quotation:Quotation; } function QuotationPage({quotation}:QuotationProps){ const [quotation, setQuotation] = useState(quotation); const [signed, setSigned] = useState(false); }
Dzieląc stan nie musimy tworzyć nowych interfejsów. Typy stanów lokalnych są często wywnioskowane przez domyślne wartości stanu.
Komponenty z haczykami to wszystkie funkcje. Możemy więc napisać ten sam komponent zwracający typ FC<P>
zdefiniowany w bibliotece React. Funkcja jawnie deklaruje swój zwracany typ, ustawiając ją zgodnie z typem props.
const QuotationPage : FC<QuotationProps> = ({quotation}) => { const [quotation, setQuotation] = useState(quotation); const [signed, setSigned] = useState(false); }
Najwyraźniej używanie TypeScriptu z hookami React jest łatwiejsze niż używanie go z klasami React. A ponieważ silne pisanie jest cennym zabezpieczeniem dla bezpieczeństwa kodu, powinieneś rozważyć użycie TypeScript, jeśli twój nowy projekt używa hooków. Zdecydowanie powinieneś używać hooków, jeśli chcesz trochę TypeScript.
Istnieje wiele powodów, dla których można uniknąć TypeScriptu, używając Reacta lub nie. Ale jeśli zdecydujesz się go użyć, zdecydowanie powinieneś również użyć haków.
Specyficzne cechy TypeScriptu odpowiednie dla hooków
W poprzednim przykładzie przechwytów React TypeScript nadal mam atrybut number w QuotationProps, ale nie ma jeszcze pojęcia, czym tak naprawdę jest ta liczba.
TypeScript daje nam długą listę typów narzędzi, a trzy z nich pomogą nam z Reactem, redukując szum wielu opisów interfejsów.
-
Partial<T>
: dowolne podklucze T -
Omit<T, 'x'>
: wszystkie klucze T z wyjątkiem kluczax
-
Pick<T, 'x', 'y', 'z'>
: Dokładniex, y, z
klucze zT
W naszym przypadku chcielibyśmy, aby Omit<Quotation, 'id'>
pominął id cytatu. Możemy stworzyć nowy typ w locie za pomocą słowa kluczowego type
.
Partial<T>
i Omit<T>
nie istnieją w większości typów języków, takich jak Java, ale bardzo pomagają w przykładach z formularzami w programowaniu front-end. Upraszcza pisanie na klawiaturze.
type QuotationProps= Omit<Quotation, id>; function QuotationPage({quotation}:QuotationProps){ const [quotation, setQuotation] = useState(quotation); const [signed, setSigned] = useState(false); // ... }
Teraz mamy ofertę bez identyfikatora. Więc może moglibyśmy zaprojektować Quotation
, a PersistedQuotation
extends
Quotation
. Ponadto z łatwością rozwiążemy niektóre powtarzające if
lub undefined
problemy. Czy nadal powinniśmy wywoływać zmienną cytat, chociaż nie jest to pełny obiekt? To wykracza poza zakres tego artykułu, ale i tak wspomnimy o tym później.

Jednak teraz jesteśmy pewni, że nie rozłożymy przedmiotu, o którym myśleliśmy, że ma number
. Użycie Partial<T>
nie zapewnia wszystkich tych gwarancji, więc używaj go z dyskrecją.
Pick<T, 'x'|'y'>
to kolejny sposób deklarowania typu w locie bez konieczności deklarowania nowego interfejsu. Jeśli komponent, po prostu edytuj tytuł oferty:
type QuoteEditFormProps= Pick<Quotation, 'id'|'title'>
Lub tylko:
function QuotationNameEditor({id, title}:Pick<Quotation, 'id'|'title'>){ ...}
Nie oceniaj mnie, jestem wielkim fanem Domain Driven Design. Nie jestem leniwy do tego stopnia, że nie chcę napisać jeszcze dwóch linijek dla nowego interfejsu. Używam interfejsów do dokładnego opisywania nazw domen, a te funkcje użytkowe dla poprawności lokalnego kodu, unikając szumów. Czytelnik będzie wiedział, że Quotation
jest interfejsem kanonicznym.
Inne zalety haków reakcyjnych
Zespół React zawsze postrzegał i traktował Reacta jako funkcjonalne ramy. Użyli klas, aby komponent mógł obsłużyć swój własny stan, a teraz hooki jako technika pozwalająca funkcji śledzić stan komponentu.
interface Place{ city:string, country:string } const initialState:Place = { city: 'Rosebud', country: 'USA' }; function reducer(state:Place, action):Partial<Place> { switch (action.type) { case 'city': return { city: action.payload }; case 'country': return { country: action.payload }; } } function PlaceForm() { const [state, dispatch] = useReducer(reducer, initialState); return ( <form> <input type="text" name="city" onChange={(event) => { dispatch({ type: 'city',payload: event.target.value}) }} value={state.city} /> <input type="text" name="country" onChange={(event) => { dispatch({type: 'country', payload: event.target.value }) }} value={state.country} /> </form> ); }
Oto przypadek, w którym korzystanie z Partial
jest bezpieczne i dobry wybór.
Chociaż funkcję można wykonać wiele razy, powiązany z nią hak useReducer
zostanie utworzony tylko raz.
Dzięki naturalnemu wyodrębnieniu funkcji redukującej z komponentu, kod może zostać podzielony na wiele niezależnych funkcji zamiast na wiele funkcji wewnątrz klasy, wszystkie powiązane ze stanem wewnątrz klasy.
Jest to wyraźnie lepsze dla testowalności — niektóre funkcje zajmują się JSX, inne zachowaniem, inne logiką biznesową i tak dalej.
Nie potrzebujesz już (prawie) komponentów wyższego rzędu. Wzorzec Render props jest łatwiejszy do napisania za pomocą funkcji.
Tak więc czytanie kodu jest łatwiejsze. Twój kod nie jest przepływem klas/funkcji/wzorców, ale przepływem funkcji. Jednakże, ponieważ twoje funkcje nie są dołączone do obiektu, nazwanie wszystkich tych funkcji może być trudne.
TypeScript to nadal JavaScript
JavaScript jest fajny, ponieważ możesz rozerwać swój kod w dowolnym kierunku. Dzięki TypeScript nadal możesz używać keyof
do zabawy z kluczami obiektów. Możesz użyć unii typów, aby stworzyć coś nieczytelnego i nie do utrzymania - nie, nie lubię tego. Możesz użyć aliasu typu, aby udawać, że ciąg jest identyfikatorem UUID.
Ale możesz to zrobić z zerowym bezpieczeństwem. Upewnij się, że tsconfig.json
ma opcję "strict":true
. Sprawdź to przed rozpoczęciem projektu, w przeciwnym razie będziesz musiał dokonać refaktoryzacji prawie każdej linii!
Toczą się debaty na temat poziomu pisania, który umieszczasz w swoim kodzie. Możesz wpisać wszystko lub pozwolić kompilatorowi wywnioskować typy. Zależy to od konfiguracji lintera i wyborów zespołu.
Ponadto nadal możesz popełniać błędy w czasie wykonywania! TypeScript jest prostszy niż Java i pozwala uniknąć problemów z kowariancją/kontrawariancją w Generics.
W tym przykładzie Zwierzę/Kot mamy listę Zwierząt, która jest identyczna z listą Kot. Niestety, to kontrakt w pierwszej linii, a nie w drugiej. Następnie dodajemy kaczkę do listy Zwierząt, więc lista Kot jest fałszywa.
interface Animal {} interface Cat extends Animal { meow: () => string; } const duck = {age: 7}; const felix = { age: 12, meow: () => "Meow" }; const listOfAnimals: Animal[] = [duck]; const listOfCats: Cat[] = [felix]; function MyApp() { const [cats , setCats] = useState<Cat[]>(listOfCats); // Here the thing: listOfCats is declared as a Animal[] const [animals , setAnimals] = useState<Animal[]>(listOfCats) const [animal , setAnimal] = useState(duck) return <div onClick={()=>{ animals.unshift(animal) // we set as first cat a duck ! setAnimals([...animals]) // dirty forceUpdate } }> The first cat says {cats[0].meow()}</div>; }
TypeScript ma tylko dwuwariantowe podejście do generyków, które jest proste i pomaga w adaptacji programistów JavaScript. Jeśli poprawnie nazwiesz zmienne, rzadko będziesz dodawać duck
do listOfCats
.
Istnieje również propozycja dodawania i wyprowadzania kontraktów dla kowariancji i kontrawariancji.
Wniosek
Niedawno wróciłem do Kotlina, który jest dobrym, pisanym językiem, a poprawne pisanie złożonych generyków było skomplikowane. Nie masz tak ogólnej złożoności z TypeScript ze względu na prostotę pisania kaczką i podejście dwuwariantowe, ale masz inne problemy. Być może nie napotkałeś wielu problemów z Angularem, zaprojektowanym zi dla TypeScript, ale miałeś je z klasami React.
TypeScript był prawdopodobnie wielkim zwycięzcą 2019 roku. Zyskał Reacta, ale także podbija świat zaplecza dzięki Node.js i jego zdolności do wpisywania starych bibliotek z dość prostymi plikami deklaracji. Zakopuje Flow, choć niektórzy idą na całość dzięki ReasonML.
Teraz uważam, że hooks+TypeScript jest przyjemniejszy i bardziej produktywny niż Angular. Nie pomyślałbym o tym sześć miesięcy temu.