Rewolucja React Hooks
Powrót do artykułówRewolucja React Hooks
React przeszedł niezwykłą transformację od swojego wprowadzenia w 2013 roku. Jako osoba, która pracowała z React przez jego główne fazy ewolucji, byłem świadkiem tego, jak wprowadzenie Hooks w React 16.8 fundamentalnie zmieniło sposób budowania interfejsów użytkownika. Ten artykuł eksploruje praktyczne implikacje tej ewolucji i dostarcza insights dla nowoczesnego rozwoju w React.
Dziedzictwo: Era komponentów klasowych
Przed Hooks, rozwój React charakteryzował się wyraźnym rozróżnieniem między stanowymi komponentami klasowymi a bezstanowymi komponentami funkcyjnymi. Ta separacja tworzyła przewidywalne wzorce, ale wprowadzała również złożoność.
Klasyczna architektura komponentów klasowych
1import React, { Component } from 'react';
2
3class UserProfile extends Component {
4 constructor(props) {
5 super(props);
6 this.state = {
7 user: null,
8 loading: true,
9 error: null
10 };
11 }
12
13 async componentDidMount() {
14 try {
15 const response = await fetch(`/api/users/${this.props.userId}`);
16 const user = await response.json();
17 this.setState({ user, loading: false });
18 } catch (error) {
19 this.setState({ error: error.message, loading: false });
20 }
21 }
22
23 componentDidUpdate(prevProps) {
24 if (prevProps.userId !== this.props.userId) {
25 this.setState({ loading: true });
26 this.fetchUser();
27 }
28 }
29
30 componentWillUnmount() {
31 // Cleanup subscriptions, cancel requests, etc.
32 this.abortController?.abort();
33 }
34
35 render() {
36 const { user, loading, error } = this.state;
37
38 if (loading) return <div>Loading...</div>;
39 if (error) return <div>Error: {error}</div>;
40
41 return (
42 <div className="user-profile">
43 <h1>{user.name}</h1>
44 <p>{user.email}</p>
45 </div>
46 );
47 }
48}
49
50export default UserProfile;
Wzorzec Container/Presentational
Ta era spopularyzowała separację odpowiedzialności przez komponenty Container i Presentational:
1// Container Component - obsługuje logikę i stan
2class UserProfileContainer extends Component {
3 state = {
4 user: null,
5 loading: true
6 };
7
8 componentDidMount() {
9 this.fetchUser();
10 }
11
12 fetchUser = async () => {
13 const user = await userService.getUser(this.props.userId);
14 this.setState({ user, loading: false });
15 };
16
17 render() {
18 return (
19 <UserProfilePresentation
20 user={this.state.user}
21 loading={this.state.loading}
22 onRefresh={this.fetchUser}
23 />
24 );
25 }
26}
27
28// Presentational Component - czyste renderowanie
29const UserProfilePresentation = ({ user, loading, onRefresh }) => {
30 if (loading) return <Spinner />;
31
32 return (
33 <div className="user-profile">
34 <h1>{user.name}</h1>
35 <button onClick={onRefresh}>Refresh</button>
36 </div>
37 );
38};
Rewolucja: Era Hooks
Wprowadzenie Hooks przekształciło rozwój React, umożliwiając komponentom funkcyjnym zarządzanie stanem i efektami ubocznymi, eliminując potrzebę komponentów klasowych w większości scenariuszy.
Nowoczesny komponent funkcyjny
1import React, { useState, useEffect } from 'react';
2
3const UserProfile = ({ userId }) => {
4 const [user, setUser] = useState(null);
5 const [loading, setLoading] = useState(true);
6 const [error, setError] = useState(null);
7
8 useEffect(() => {
9 const abortController = new AbortController();
10
11 const fetchUser = async () => {
12 try {
13 setLoading(true);
14 const response = await fetch(`/api/users/${userId}`, {
15 signal: abortController.signal
16 });
17 const userData = await response.json();
18 setUser(userData);
19 setError(null);
20 } catch (err) {
21 if (err.name !== 'AbortError') {
22 setError(err.message);
23 }
24 } finally {
25 setLoading(false);
26 }
27 };
28
29 fetchUser();
30
31 return () => {
32 abortController.abort();
33 };
34 }, [userId]);
35
36 if (loading) return <div>Loading...</div>;
37 if (error) return <div>Error: {error}</div>;
38
39 return (
40 <div className="user-profile">
41 <h1>{user.name}</h1>
42 <p>{user.email}</p>
43 </div>
44 );
45};
46
47export default UserProfile;
Custom Hooks: Rewolucja logiki wielokrotnego użytku
Jedną z najpotężniejszych funkcji Hooks jest możliwość wyodrębniania logiki komponentów do wielokrotnego użytku custom hooks.
Tworzenie Custom Hook do pobierania danych
1import { useState, useEffect, useCallback } from 'react';
2
3const useApi = (url, dependencies = []) => {
4 const [data, setData] = useState(null);
5 const [loading, setLoading] = useState(true);
6 const [error, setError] = useState(null);
7
8 const fetchData = useCallback(async () => {
9 try {
10 setLoading(true);
11 setError(null);
12
13 const response = await fetch(url);
14 if (!response.ok) {
15 throw new Error(`HTTP error! status: ${response.status}`);
16 }
17
18 const result = await response.json();
19 setData(result);
20 } catch (err) {
21 setError(err.message);
22 } finally {
23 setLoading(false);
24 }
25 }, [url]);
26
27 useEffect(() => {
28 fetchData();
29 }, [fetchData, ...dependencies]);
30
31 const refetch = useCallback(() => {
32 fetchData();
33 }, [fetchData]);
34
35 return { data, loading, error, refetch };
36};
37
38// Użycie w komponencie
39const UserProfile = ({ userId }) => {
40 const { data: user, loading, error, refetch } = useApi(
41 `/api/users/${userId}`,
42 [userId]
43 );
44
45 if (loading) return <div>Loading...</div>;
46 if (error) return <div>Error: {error}</div>;
47
48 return (
49 <div className="user-profile">
50 <h1>{user.name}</h1>
51 <p>{user.email}</p>
52 <button onClick={refetch}>Refresh</button>
53 </div>
54 );
55};
Optymalizacja wydajności z Hooks
Nowoczesny React dostarcza potężnych narzędzi optymalizacji, które były złożone lub niemożliwe z komponentami klasowymi.
useMemo i useCallback
1import React, { useState, useMemo, useCallback } from 'react';
2
3const ExpensiveComponent = ({ items, filter, onItemClick }) => {
4 // Memoize kosztownych kalkulacji
5 const filteredItems = useMemo(() => {
6 return items.filter(item =>
7 item.name.toLowerCase().includes(filter.toLowerCase())
8 ).sort((a, b) => a.priority - b.priority);
9 }, [items, filter]);
10
11 // Memoize event handlers
12 const handleItemClick = useCallback((itemId) => {
13 onItemClick(itemId);
14 }, [onItemClick]);
15
16 const expensiveValue = useMemo(() => {
17 return filteredItems.reduce((acc, item) => acc + item.value, 0);
18 }, [filteredItems]);
19
20 return (
21 <div>
22 <div>Total Value: {expensiveValue}</div>
23 {filteredItems.map(item => (
24 <ItemComponent
25 key={item.id}
26 item={item}
27 onClick={handleItemClick}
28 />
29 ))}
30 </div>
31 );
32};
33
34const ItemComponent = React.memo(({ item, onClick }) => {
35 return (
36 <div
37 className="item"
38 onClick={() => onClick(item.id)}
39 >
40 <h3>{item.name}</h3>
41 <p>{item.description}</p>
42 </div>
43 );
44});
Ewolucja Context API
Kombinacja Context API i Hooks dostarcza potężną alternatywę dla zewnętrznych bibliotek zarządzania stanem.
Nowoczesny wzorzec Context
1import React, { createContext, useContext, useReducer, useMemo } from 'react';
2
3const AuthContext = createContext();
4
5const authReducer = (state, action) => {
6 switch (action.type) {
7 case 'LOGIN_START':
8 return { ...state, loading: true, error: null };
9
10 case 'LOGIN_SUCCESS':
11 return {
12 ...state,
13 loading: false,
14 user: action.payload,
15 isAuthenticated: true
16 };
17
18 case 'LOGIN_ERROR':
19 return {
20 ...state,
21 loading: false,
22 error: action.payload,
23 isAuthenticated: false
24 };
25
26 case 'LOGOUT':
27 return {
28 ...state,
29 user: null,
30 isAuthenticated: false
31 };
32
33 default:
34 return state;
35 }
36};
37
38export const AuthProvider = ({ children }) => {
39 const [state, dispatch] = useReducer(authReducer, {
40 user: null,
41 isAuthenticated: false,
42 loading: false,
43 error: null
44 });
45
46 const login = async (credentials) => {
47 dispatch({ type: 'LOGIN_START' });
48 try {
49 const response = await authService.login(credentials);
50 dispatch({ type: 'LOGIN_SUCCESS', payload: response.user });
51 } catch (error) {
52 dispatch({ type: 'LOGIN_ERROR', payload: error.message });
53 }
54 };
55
56 const logout = () => {
57 authService.logout();
58 dispatch({ type: 'LOGOUT' });
59 };
60
61 const value = useMemo(() => ({
62 ...state,
63 login,
64 logout
65 }), [state]);
66
67 return (
68 <AuthContext.Provider value={value}>
69 {children}
70 </AuthContext.Provider>
71 );
72};
73
74export const useAuth = () => {
75 const context = useContext(AuthContext);
76 if (!context) {
77 throw new Error('useAuth must be used within an AuthProvider');
78 }
79 return context;
80};
Kluczowe wnioski
Ewolucja od komponentów klasowych do Hooks reprezentuje więcej niż zmianę składniową—to fundamentalna zmiana w sposobie myślenia o architekturze aplikacji React:
- Uproszczony model mentalny: Hooks zapewniają bardziej bezpośredni sposób pracy z funkcjami React
- Lepsze ponowne użycie kodu: Custom hooks umożliwiają dzielenie logiki bez złożoności wrapperów komponentów
- Ulepszona wydajność: Zarówno pod względem rozmiaru bundle'a, jak i charakterystyk runtime
- Wzbogacone doświadczenie developera: Łatwiejsze testowanie, debugowanie i organizacja kodu
Dla zespołów nadal używających komponentów klasowych, polecam stopniowe podejście do migracji, zaczynając od nowych komponentów i stopniowo aktualizując istniejące, gdy są potrzebne modyfikacje.