Redux i React: czego nie robić
Kiedy budujesz aplikacje React, małe projekty mogą okazać się nieco bardziej elastyczne pod kątem architektury kodu niż duże. Chociaż nie ma nic złego w budowaniu małych aplikacji w oparciu o najlepsze praktyki przeznaczone dla większych aplikacji, stosowanie wielkich decyzji może okazać się zbędne. Im mniejsza jest aplikacja, tym bardziej możemy być leniwi.
Jednakże, najlepsze praktyki w tym artykule można wykorzystać do dowolnych rozmiarów aplikacji React.
Jeśli nigdy nie miałeś doświadczenia w budowaniu aplikacji produkcyjnych, to ta część pomoże Ci się do niej przygotować. Najgorszą rzeczą, która może Ci się przydarzyć, to budowanie aplikacji w pracy, tylko po to, aby zdać sobie sprawę, że trzeba zrefaktoryzować duże ilości kodu, aby aplikacja była bardziej skalowalna i łatwiejsza do utrzymania - zwłaszcza jeśli brakuje testów jednostkowych!
Zaufaj mi. Sam popełniłem ten błąd. Na początku myślałem, że wszystko idzie gładko. Myślałem, że tylko dlatego, że moja aplikacja działa i wydawało mi się że jest szybka, oznacza to że wykonuję świetną pracę rozwijając i utrzymując swój kod. Wiedziałem, jak używać Redux i jak sprawić, by komponenty interfejsu użytkownika współdziałały ze sobą zgodnie z oczekiwaniami.Reduktory (Reducers) i akcje (actions) były dla mnie zrozumiałym konceptem. Czułem, że nic mnie nie powstrzyma.
Aż nastała przyszłość.
Kilka miesięcy i jakieś 15 funkcji później, sprawy zaczęły wymykać się spod kontroli. Mój kod wykorzystujący Redux przestał być łatwy w utrzymaniu.
Spytasz "Dlaczego?"
"Przecież byłeś niepowstrzymany?"
Ja też tak myślałem, a skończyło się na czekaniu aż tykająca bomba wybuchnie. Redux można bardzo łatwo utrzymywać, jeśli został prawidłowo użyty, nawet w dużych projektach..
Czytaj dalej, aby dowiedzieć się, czego nie robić, jeśli planujesz budowę skalowalnych aplikacji webowych React.
1. Umieszczanie akcji i stałych w jednym miejscu
Możesz zobaczyć kilka tutoriali Redux, które umieszczają stałe i wszystkie akcje w jednym miejscu. Jednakże, może to szybko stać się kłopotliwe, ponieważ aplikacja staje się coraz większa. Stałe powinny znajdować się w osobnej lokalizacji, takiej jak ./src/constants
, tak aby było jedno miejsce do przeszukania zamiast wielu lokalizacji.
W każdym razie, jest zdecydowanie w porządku, aby utworzyć osobny plik akcji reprezentujący to, z czym lub jak będzie używany, enkapsulując bezpośrednio powiązane akcje. Jeśli tworzyłeś nową grę zręcznościową/RPG, która ma kilka klas postaci: wojownik, czarodziejka i łucznik, będzie to o wiele łatwiejsze, jeśli umieścisz swoje działania w ten sposób:
src/actions/warrior.js
src/actions/sorceress.js
src/actions/archer.js
Raczej niż coś takiego:
src/actions/classes.js
Jeśli aplikacja staje się ogromna, prawdopodobnie lepiej będzie, spróbować z czymś takim:
src/actions/warrior/skills.js
src/actions/sorceress/skills.js
src/actions/archer/skills.js
Gdyby to był realistyczny scenariusz, być może skończylibyśmy z taką organizacją plików:
src/actions/warrior/skills.js
src/actions/warrior/quests.js.
src/actions/warrior/equipping.js
src/actions/sorceress/skills.js
src/actions/sorceress/quests.js.
src/actions/sorceress/equipping.js
src/actions/archer/skills.js
src/actions/archer/quests.js.
src/actions/archer/equipping.js
Przykład tego, jak wyglądałyby akcje czarodziejki:
src/actions/sorceress/skills
import { CAST_FIRE_TORNADO, CAST_LIGHTNING_BOLT } from '../constants/sorceress'
export const castFireTornado = (target) => ({
type: CAST_FIRE_TORNADO,
target,
})
export const castLightningBolt = (target) => ({
type: CAST_LIGHTNING_BOLT,
target,
})
src/actions/sorceress/equipping
import * as consts from '../constants/sorceress'
export const equipStaff = (staff, enhancements) => {...}
export const removeStaff = (staff) => {...}
export const upgradeStaff = (slot, enhancements) => {
return (dispatch, getState, { api }) => {
// Grab the slot in our equipment screen to grab the staff reference
const state = getState()
const currentEquipment = state.classes.sorceress.equipment.current
const staff = currentEquipment[slot]
const isMax = staff.level >= 9
if (isMax) {
return
}
dispatch({ type: consts.UPGRADING_STAFF, slot })
api.upgradeEquipment({
type: 'staff',
id: currentEquipment.id,
enhancements,
})
.then((newStaff) => {
dispatch({ type: consts.UPGRADED_STAFF, slot, staff: newStaff })
})
.catch((error) => {
dispatch({ type: consts.UPGRADE_STAFF_FAILED, error })
})
}
}
Powodem, dla którego to robimy jest to, że najprawdopodobniej zawsze będą nowe funkcje do dodania i musisz się na nie przygotować, w miarę jak Twoje pliki coraz bardziej puchną.
Może się to wydawać zbędne na początku, ale te podejścia zaczną się opłacać, wraz z wzrostem rozmiaru projektu.
2. Umieszczanie reduktorów w jednym miejscu
Kiedy moje reduktory zaczynają tak wyglądać:
const equipmentReducers = (state, action) => {
switch (action.type) {
case consts.UPGRADING_STAFF:
return {
...state,
classes: {
...state.classes,
sorceress: {
...state.classes.sorceress,
equipment: {
...state.classes.sorceress.equipment,
isUpgrading: action.slot,
},
},
},
}
case consts.UPGRADED_STAFF:
return {
...state,
classes: {
...state.classes,
sorceress: {
...state.classes.sorceress,
equipment: {
...state.classes.sorceress.equipment,
isUpgrading: null,
current: {
...state.classes.sorceress.equipment.current,
[action.slot]: action.staff,
},
},
},
},
}
case consts.UPGRADE_STAFF_FAILED:
return {
...state,
classes: {
...state.classes,
sorceress: {
...state.classes.sorceress,
equipment: {
...state.classes.sorceress.equipment,
isUpgrading: null,
},
},
},
}
default:
return state
}
}
Oczywiście bardzo szybko może powstać duży bałagan, więc najlepiej jest zachować prostą i jak najbardziej płaską strukturę stanu lub spróbować skomponować wszystkie reduktory.
Zgrabną sztuczką jest stworzenie reduktora wyższego rzędu, który generuje reduktory, mapując każdy opakowany reduktor do obiektu mapującego z typów akcji do handlerów.
3. Nadawanie słabych nazw swoim zmiennym
Nadawanie nazw swoim zmiennym brzmi jak coś prostego, ale może to być jedna z najtrudniejszych rzeczy do opanowania przy pisaniu kodu.
Jest to zasadniczo praktyka czystego kodowania, a powodem, dla którego termin ten istnieje, jest fakt jak ważne jest stosowanie tego w praktyce. Słabe nazewnictwo zmiennych to dobry sposób na to, aby członkowie zespołu i przyszły ty cierpieli!
Czy kiedykolwiek próbowałeś edytować czyjś kod i miałeś problemy ze zrozumieniem tego, co kod próbował zrobić? Czy kiedykolwiek sprawdziłeś czyjś kod i okazało się, że działa on inaczej niż się spodziewałeś?
Założę się, że autor pisał brudny kod.
Najgorsza sytuacja, w jakiej możemy się znaleźć, to konieczność przejścia przez to w dużej aplikacji, gdzie dzieje się to w wielu obszarach.
Pozwól, że przedstawię sytuację z życia wziętą, której sam doświadczyłem:
Kiedy dostałem zadanie dodania i pokazania dodatkowych informacji o każdym z lekarzy w tabeli, zacząłem edytować istniejący hook React w kodzie aplikacji, przenieść przed hook. W momencie, gdy wybrali (klikając) lekarza, informacje o lekarzu zostały zapisane w jakimś stanie, abyśmy mogli wysłać informacje w następnym żądaniu do API.
Wszystko szło dobrze, z wyjątkiem tego, że spędzałem więcej czasu niż powinienem na szukaniu gdzie ta część była w kodzie.
W tym momencie w mojej głowie szukałem słów takich jak info, dataToSend, dataObject, doctorProfile lub czegokolwiek związanego z danymi, które właśnie zostały zebrane. 10-15 minut później znalazłem część, która zaimplementowała ten przepływ i obiekt, w którym został umieszczony nosił nazwę paymentObject
. Kiedy myślę o obiektach płatności, myślę o CVV, ostatnich 4 cyfrach, kodzie pocztowym itp. Spośród 11 właściwości tylko trzy były związane z płatnością: sposób naliczania opłat, identyfikator profilu płatności i kupony.
Nie pomógł również fakt jak bardzo niezręczne było łączenie tego kodu z moimi zmianami.
Krótko mówiąc, postaraj się nie nazywać swoich funkcji lub zmiennych w ten sposób:
import React from 'react'
class App extends React.Component {
state = { data: null }
// Notify what?
notify = () => {
if (this.props.user.loaded) {
if (this.props.user.profileIsReady) {
toast.alert(
'You are not approved. Please come back in 15 minutes or you will be deleted.',
{
position: 'bottom-right',
timeout: 15000,
},
)
}
}
}
render() {
return this.props.render({
...this.state,
notify: this.notify,
})
}
}
export default App
4. Zmiana struktury danych/typu w połowie drogi
Jednym z największych błędów, jakie kiedykolwiek popełniłem, była zmiana struktury danych/typu czegoś podczas już ustalonego przepływu aplikacji. Nowa struktura danych spowodowałaby duży wzrost wydajności, ponieważ wykorzystywała obiekty do wyłuskiwania danych w pamięci zamiast mapowania tablic. Ale było już za późno.
Proszę, nie rób tego, chyba że naprawdę znasz wszystkie obszary, na które będzie to miało wpływ.
Jakie są niektóre konsekwencje?
Jeśli coś zmieni się z tablicy na obiekt, wiele obszarów aplikacji może przestać działać. Popełniłem największy błąd myśląc, że każda część aplikacji, na którą miałem wpływ zaplanowana w moim umyśle zmiana struktury danych, ale zawsze pozostanie to jedno miejsce, które zostało pominięte.
6. Kodowanie bez użycia snippetów
Kiedyś byłem fanem Atom, ale przełączyłem się na VScode, ponieważ dostarczał on funkcji, które uważam za bardzo cenne w procesie rozwoju jak Project Snippets.
Jeśli używasz VSCode, bardzo polecam pobrać tą wtyczkę, jeśli jeszcze tego nie zrobiłeś. Rozszerzenie to umożliwia deklarowanie niestandardowych fragmentów dla każdego obszaru roboczego dla danego projektu. Działa dokładnie tak samo, jak wbudowana funkcja snippetów użytkownika, która domyślnie pojawia się w VScode, poza tym, że tworzy folder .vscode/snippets/
wewnątrz projektu w ten sposób:
7. Ignorowanie jednostki/E2E/testów integracyjnych
W miarę powiększania się aplikacji, edycja istniejącego kodu bez konieczności przeprowadzania jakichkolwiek testów staje się coraz bardziej przerażająca. Możesz zakończyć edycję pliku znajdującego się w src/x/y/z/ i podjąć decyzję o wgraniu tego na produkcję, jednak jeśli zmiana dotyczy innej części aplikacji, której nie zauważyłeś, błąd pozostanie tam, dopóki prawdziwy użytkownik nie złapie go przeglądając stronę, ponieważ nie będziesz miał żadnych testów, które mogłyby cię wcześniej ostrzec.
8. Pominięcie fazy burzy mózgów
Deweloperzy często pomijają fazę burzy mózgów, ponieważ wybierają kodowanie, zwłaszcza gdy mają tydzień na opracowanie nowej funkcji. Jednak z doświadczenia wynika, że jest to najważniejszy krok i pozwala zaoszczędzić Tobie i Twojemu zespołowi dużo czasu w przyszłości.
Po co zawracać sobie głowę burzą mózgów?
Im bardziej złożona aplikacja, tym więcej programistów musi zarządzać niektórymi częściami aplikacji. Burza mózgów pomaga wyeliminować znaczą część refaktoryzacji, ponieważ już zaplanowałeś, co może pójść nie tak. Często zdarza się, że programiści nie mają czasu, aby usiąść i zastosować wszystkie zgrabne praktyki w celu dalszego ulepszania aplikacji.
Dlatego burza mózgów jest ważna. Pozwala zastanowić się nad architekturą i potrzebnymi ulepszeniami, co pozwoli zająć się tym wszystkim od początku, stosując strategiczne podejście.. Nie wpadaj w nawyk bycia zbyt pewnym siebie i planowania wszystkiego w swojej głowie. Jeśli to zrobisz, nie będziesz w stanie zapamiętać wszystkiego. Kiedy zrobisz coś złego, efekt domina pociągnie za sobą więcej problemów.
Burza mózgów ułatwi pracę również twojemu zespołowi. Jeśli któryś z nich kiedykolwiek utknie w jakimś zadaniu, może odwołać się do burzy mózgów i być może tak znajdzie właściwe rozwiązanie.
Notatki, które robisz podczas burzy mózgów, mogą również służyć Tobie i Twojemu zespołowi jako agenda i pomóc w łatwym zapewnieniu spójnego obrazu Twoich bieżących postępów podczas opracowywania aplikacji.
9. Nie określanie zawczasu elementów interfejsu użytkownika.
Jeśli zamierzasz zacząć budować swoją aplikację, powinieneś zdecydować, jak chcesz, aby Twoja aplikacja wyglądała. Dostępnych jest kilka narzędzi, które pomogą Ci w tworzeniu własnych mockupów.
Jednym z narzędzi, o którym często słyszę, jest Moqups. Jest szybki, nie wymaga żadnych wtyczek i jest zbudowany w HTML5 i JavaScript.
Wykonanie tego kroku jest bardzo pomocne w przekazaniu zarówno informacji, jak i danych, które znajdą się na tworzonych przez Ciebie stronach. Rozwijanie aplikacji będzie prostsze.
10. Nie planowanie przepływu danych
Prawie każdy komponent Twojej aplikacji będzie kojarzony z jakimiś danymi. Niektóre z nich będą korzystać z własnego źródła danych, ale większość z nich będzie dostarczana z miejsca znajdującego się gdzieś wyżej w drzewie. W przypadku części aplikacji, w których dane są współdzielone z więcej niż jednym komponentem, dobrym pomysłem jest udostępnienie tych danych wyżej w drzewie, gdzie będą one działać jako centralne drzewo stanu. To tutaj moc Redux przychodzi na ratunek :)
Doradzam sporządzenie listy, zawierającej, w jaki sposób dane będą przepływać przez całą aplikację. Pomoże Ci to stworzyć mocny mentalny i spisany model Twojej aplikacji. Stworzenie reduktora na tej podstawie będzie o wiele łatwiejsze.
11. Nieużywanie akcesorów
W miarę jak aplikacja staje się coraz większa, zwiększa się również liczba komponentów. A gdy liczba komponentów wzrośnie, zwiększy się również liczba używanych selektorów (react-redux ^v7.1) lub mapStateToProps. Jeśli znajdziesz swoje komponenty lub hooki często używając wycinków stanu jak useSelector((state) => state.app.user.profile.demographics.languages.main) w kilku częściach aplikacji, nadszedł czas, aby zacząć myśleć o tworzeniu akcesorów we współdzielonej lokalizacji, by komponenty/hooki mogły łatwo ich używać. Funkcje te mogą być funkcjami filtrującymi, parserami lub innymi funkcjami przekształcania danych.
Oto kilka przykładów:
src/accessors
export const getMainLanguages = (state) =>
state.app.user.profile.demographics.languages.main
wersja z connect
src/components/ViewUserLanguages
import React from 'react'
import { connect } from 'react-redux'
import { getMainLanguages } from '../accessors'
const ViewUserLanguages = ({ mainLanguages }) => (
<div>
<h1>Good Morning.</h1>
<small>Here are your main languages:</small>
<hr />
{mainLanguages.map((lang) => (
<div>{lang}</div>
))}
</div>
)
export default connect((state) => ({
mainLanguages: getMainLanguages(state),
}))(ViewUserLanguages)
wersja z useSelector
src/components/ViewUserLanguages
import React from 'react'
import { useSelector } from 'react-redux'
import { getMainLanguages } from '../accessors'
const ViewUserLanguages = ({ mainLanguages }) => {
const mainLanguages = useSelector(getMainLanguages)
return (
<div>
<h1>Good Morning.</h1>
<small>Here are your main languages:</small>
<hr />
{mainLanguages.map((lang) => (
<div>{lang}</div>
))}
</div>
)
}
export default ViewUserLanguages
Bardzo ważne jest również, aby funkcje te były niemutowalne i wolne od skutków ubocznych, jak opisano tutaj.
Alternatywnie, kolejnym popularnym rozwiązaniem jest reselect specjalnie stworzony i zoptymalizowany pod Redux, jak wyjaśniono tutaj. Jeśli używasz go tak samo jak którejkolwiek z wyżej wymienionych funkcji dostępowych, będziesz miał dodatkową korzyść w postaci obliczonych wartości w pamięci podręcznej - gdzie funkcje te będą obliczane tylko wtedy, gdy jeden z ich argumentów ulegnie zmianie. Im droższe są obliczenia w Twoich komponentach, tym bardziej zalecam to rozwiązanie. (Dzięki Healqqq).
12. Niekontrolowanie przepływu w propsach przy użyciu destrukturyzacji i spread attributes
Jakie są korzyści płynące z używania props.something
a jakie z something
?
Bez destrukturyzacji:
const Display = (props) => <div>{props.something}</div>
const Display = ({ something }) => <div>{something}</div>
Dzięki destrukturyzacji nie tylko zwiększasz czytelność swojego kodu dla siebie i innych, ale także podejmujesz bezpośrednie decyzje dotyczące tego, co wchodzi i co wychodzi. Kiedy inni programiści będą edytować Twój kod w przyszłości, nie będą musieli skanować każdej linii kodu w Twojej metodzie renderowania, aby znaleźć wszystkie propsy, których używa dany komponent.
Zyskujesz również na możliwości deklarowania domyślnego propa od samego początku bez konieczności dodawania kolejnych wierszy kodu:
const Display = ({ something = 'apple' }) => <div>{something}</div>
Być może widziałeś już coś takiego:
const Display = (props) => (
<Agenda {...props}>
{' '}
// forward other props to Agenda
<h2>Today is {props.date}</h2>
<hr />
<div>
<h3>Here your list of todos:</h3>
{props.children}
</div>
</Agenda>
)
Jest to nie tylko trochę trudniejsze do odczytania, ale również występuje w tym komponencie niezamierzony błąd. Jeśli App
również renderuje dzieci, masz props.children
renderowane dwa razy. To tworzy duplikaty. Kiedy pracujesz z zespołem programistów a nie sam, istnieje szansa, że te błędy mogą zdarzyć się przypadkowo, zwłaszcza jeśli programiści nie są wystarczająco ostrożni.
Poprzez destrukturyzację propsów, komponent może dotrzeć bezpośrednio do swojego celu, zmniejszając tym samym prawdopodobieństwo wystąpienia błędów:
const Display = ({ children, date, ...props }) => (
<Agenda {...props}>
{' '}
// forward other props to Agenda
<h2>Today is {date}</h2>
<hr />
<div>
<h3>Here your list of todos:</h3>
{children}
</div>
</Agenda>
)
Oryginał tekstu w języku angielskim przeczytasz tutaj.