Mam swoją niepisaną wewnętrzną regułę, gdy komponent przechowuje ponad 4 różne stany, to prawdopodobnie nie do końca wiem co robię i powinienem się douczyć albo muszę wydzielić logikę do useReducer.
Obecnie wiele problemów ze stanem rozwiązują bardzo pomocne biblioteki jak React Hook Form lub Tanstack Query, ale na potrzeby pokazania jak otypować reducer bezmyślnie przyjmiemy, że komponent jest super przemyślany, a jedyny refactor jaki jest potrzebny to pozbycie się tych stanów.
Powiedzmy, że mamy magiczny widget, który pojawia się w naszej aplikacji podczas określonych warunków i wyświetla pewnie dane statystyczne.
// widget.tsx
const [isVisible, setIsVisible] = useState(false)
const [data, setData] = useState<FetchedProject>(null)
const [dataType, setDataType] = useState<DataType>("all")
const [activeTab, setActiveTab] = useState<ActiveTab>("overview")
To dość sporo stanów nieprawdaż? Na szczęście możemy trochę posprzątać.
// widget-reducer.ts
type DataType = "all" | "active" | "archived"
type ActiveTab = "overview" | "details" | "settings"
type FetchedProject = Project[] | null
export type WidgetState = {
isVisible: boolean
data: FetchedProject
dataType: DataType
activeTab: ActiveTab
}
export const initialWidgetState: WidgetState = {
isVisible: false,
data: null,
dataType: "all",
activeTab: "overview",
}
export type WidgetAction =
| {
type: "SET_DATA"
payload: FetchedProject
}
| {
type: "SET_IS_VISIBLE"
}
| {
type: "SET_DATA_TYPE"
payload: DataType
}
| {
type: "SET_ACTIVE_TAB"
payload: ActiveTab
export const widgetReducer = (state: WidgetState, action: WidgetAction) => {
switch (action.type) {
case "SET_DATA":
return { ...state, data: action.payload }
case "SET_IS_VISIBLE":
return { ...state, isVisible: !state.isVisible }
case "SET_DATA_TYPE": {
const newDataType = action.payload
const filtered =
newDataType === "all"
? state.data
: state.data?.filter((p) => p.dataType === newDataType)
return { ...state, dataType: newDataType, data: filteredData ?? null }
}
case "SET_ACTIVE_TAB":
return { ...state, activeTab: action.payload }
default:
throw new Error("Unhandled action type in WidgetReducer")
}
}
Teraz możemy wyczyścić useState z komponentu i zastąpić go useReducerem.
// widget.tsx
const [state, dispatch] = useReducer(widgetReducer, initialWidgetState)
Oraz przykładowe funkcje wywołujące zmiany stanu.
// widget.tsx
const handleWidgetVisibility = () => {
dispatch({ type: "SET_IS_VISIBLE" })
}
const handleDataTypeChange = (dataType: DataType) => {
dispatch({ type: "SET_DATA_TYPE", payload: dataType })
}
const handleActiveTabChange = (tab: ActiveTab) =>
dispatch({ type: "SET_ACTIVE_TAB", payload: tab })
W ten sposób schowaliśmy szczegóły implementacyjne stanu wewnątrz reducera, oddzieliliśmy część logiki od warstwy reprezentacyjnej komponentu i finalnie zwiększyliśmy czytelność kodu. Przykład jest dość prymitywny, ale pożądany efekt to wywołanie wyłącznie prostej akcji o biznesowej semantyce, a magia dzieje się pod spodem.
Powrót do artykułów