Mohammad Faisal
Mohammad FaisalSenior Software Engineer @ Advanced Mobility Analytics

SOLIDne podpowiedzi jak wyczyścić kod w REACT

Przekonaj się jak wdrażanie SRP wpływa na przejrzystość i objętość kodu. Poznaj różnice na przykładzie REACTa.
25.10.20215 min
SOLIDne podpowiedzi jak wyczyścić kod w REACT

Zasady SOLID mają być głównie przewodnikiem dla osób, które tworzą oprogramowanie i chcą robić to dobrze. Dla ludzi, którzy są dumni z budowania pięknie zaprojektowanego kodu, który ma przetrwać próbę czasu.

Dzisiaj zaczniemy od przykładu złego kodu, zastosujemy pierwszą zasadę SOLID i zobaczymy, jak to może nam pomóc w pisaniu małych, pięknych i czystych komponentów React z jasno określonymi obowiązkami. Zaczynajmy.

Co to jest zasada pojedynczej odpowiedzialności?

Zasada pojedynczej odpowiedzialności mówi nam, że każda klasa lub komponent powinny być odpowiedzialne za jedną rzecz. Komponenty powinny robić tylko jedną rzecz, a do tego robić to dobrze.

Wykonajmy refaktoryzację złego, ale działającego fragmentu kodu i uczyńmy go czystszym i lepszym dzięki zastosowaniu tej zasady.

Zacznijmy od złego przykładu — zobaczmy najpierw kod, który narusza tę zasadę. Komentarze zostały dodane w celu lepszego zrozumienia:

import React, {useEffect, useReducer, useState} from "react";

const initialState = {
    isLoading: true
};

// COMPLEX STATE MANAGEMENT
function reducer(state, action) {
    switch (action.type) {
        case 'LOADING':
            return {isLoading: true};
        case 'FINISHED':
            return {isLoading: false};
        default:
            return state;
    }
}

export const SingleResponsibilityPrinciple = () => {

    const [users , setUsers] = useState([])
    const [filteredUsers , setFilteredUsers] = useState([])
    const [state, dispatch] = useReducer(reducer, initialState);

    const showDetails = (userId) => {
        const user = filteredUsers.find(user => user.id===userId);
        alert(user.contact)
    }

    // REMOTE DATA FETCHING
    useEffect(() => {
        dispatch({type:'LOADING'})
        fetch('https://jsonplaceholder.typicode.com/users')
            .then(response => response.json())
            .then(json => {
                dispatch({type:'FINISHED'})
                setUsers(json)
            })
    },[])

    // PROCESSING DATA
    useEffect(() => {
        const filteredUsers = users.map(user => {
            return {
                id: user.id,
                name: user.name,
                contact: `${user.phone} , ${user.email}`
            };
        });
        setFilteredUsers(filteredUsers)
    },[users])

    // COMPLEX UI RENDERING
    return <>
        <div> Users List</div>
        <div> Loading state: {state.isLoading? 'Loading': 'Success'}</div>
        {users.map(user => {
            return <div key={user.id} onClick={() => showDetails(user.id)}>
                <div>{user.name}</div>
                <div>{user.email}</div>
            </div>
        })}
    </>
}

Co robi ten kod

Jest to komponent funkcyjny, w którym pobieramy dane z odległego źródła, filtrujemy je, a następnie pokazujemy w UI. Wykrywamy również stan ładowania wywołania API.

Skróciłem nieco ten przykład, aby był bardziej zrozumiały. Resztę można jednak znaleźć w tym samym komponencie prawie wszędzie! Tutaj dość sporo się dzieje:

  1. Pobieranie zdalnych danych
  2. Filtrowanie danych
  3. Kompleksowe zarządzanie stanem
  4. Kompleksowa funkcjonalność UI


Zbadajmy więc, jak możemy poprawić projekt kodu i uczynić go stylowym.

1. Przeniesienie logiki przetwarzania danych na zewnątrz

Nigdy nie przechowuj swoich wywołań HTTP wewnątrz komponentu. To jest podstawowa zasada! Istnieje kilka strategii, które możesz zastosować, aby usunąć ten kod z komponentu.

Powinieneś przynajmniej utworzyć niestandardowy hook i przenieść tam swoją logikę pobierania danych. Na przykład, możemy utworzyć hook o nazwie useGetRemoteData, który wygląda tak jak poniżej:

import {useEffect, useReducer, useState} from "react";

const initialState = {
    isLoading: true
};

function reducer(state, action) {
    switch (action.type) {
        case 'LOADING':
            return {isLoading: true};
        case 'FINISHED':
            return {isLoading: false};
        default:
            return state;
    }
}

export const useGetRemoteData = (url) => {

    const [users , setUsers] = useState([])
    const [state, dispatch] = useReducer(reducer, initialState);

    const [filteredUsers , setFilteredUsers] = useState([])


    useEffect(() => {
        dispatch({type:'LOADING'})
        fetch('https://jsonplaceholder.typicode.com/users')
            .then(response => response.json())
            .then(json => {
                dispatch({type:'FINISHED'})
                setUsers(json)
            })
    },[])

    useEffect(() => {
        const filteredUsers = users.map(user => {
            return {
                id: user.id,
                name: user.name,
                contact: `${user.phone} , ${user.email}`
            };
        });
        setFilteredUsers(filteredUsers)
    },[users])

    return {filteredUsers , isLoading: state.isLoading}
}


Teraz nasz główny komponent będzie wyglądał tak:

import React from "react";
import {useGetRemoteData} from "./useGetRemoteData";

export const SingleResponsibilityPrinciple = () => {

    const {filteredUsers , isLoading} = useGetRemoteData()

    const showDetails = (userId) => {
        const user = filteredUsers.find(user => user.id===userId);
        alert(user.contact)
    }

    return <>
        <div> Users List</div>
        <div> Loading state: {isLoading? 'Loading': 'Success'}</div>
        {filteredUsers.map(user => {
            return <div key={user.id} onClick={() => showDetails(user.id)}>
                <div>{user.name}</div>
                <div>{user.email}</div>
            </div>
        })}
    </>
}


Spójrz! Nasz komponent jest teraz mniejszy i łatwiejszy do zrozumienia! Jest to najprostsza i pierwsza rzecz, jaką możesz zrobić w zagmatwanej bazie kodu.

Ale stać nas na więcej.

2. Hook wielokrotnego użytku do pobierania danych

Teraz gdy widzimy nasz hook useGetRemoteData, widzimy, że wykonuje on dwie rzeczy:

  1. Pobieranie danych ze zdalnego źródła
  2. Filtrowanie danych


Wyodrębnijmy logikę pobierania zdalnych danych do osobnego hooka o nazwie useHttpGetRequest, który pobiera URL jako komponent:

import {useEffect, useReducer, useState} from "react";
import {loadingReducer} from "./LoadingReducer";

const initialState = {
    isLoading: true
};

export const useHttpGetRequest = (URL) => {

    const [users , setUsers] = useState([])
    const [state, dispatch] = useReducer(loadingReducer, initialState);

    useEffect(() => {
        dispatch({type:'LOADING'})
        fetch(URL)
            .then(response => response.json())
            .then(json => {
                dispatch({type:'FINISHED'})
                setUsers(json)
            })
    },[])

    return {users , isLoading: state.isLoading}

}


Usunęliśmy również logikę reduktora i przenieśliśmy do osobnego pliku:

export function loadingReducer(state, action) {
    switch (action.type) {
        case 'LOADING':
            return {isLoading: true};
        case 'FINISHED':
            return {isLoading: false};
        default:
            return state;
    }
}


Więc teraz nasz useGetRemoteData staje się:

import {useEffect, useState} from "react";
import {useHttpGetRequest} from "./useHttpGet";
const REMOTE_URL = 'https://jsonplaceholder.typicode.com/users'

export const useGetRemoteData = () => {
    const {users , isLoading} = useHttpGetRequest(REMOTE_URL)
    const [filteredUsers , setFilteredUsers] = useState([])

    useEffect(() => {
        const filteredUsers = users.map(user => {
            return {
                id: user.id,
                name: user.name,
                contact: `${user.phone} , ${user.email}`
            };
        });
        setFilteredUsers(filteredUsers)
    },[users])

    return {filteredUsers , isLoading}
}


Dużo czyściej, prawda? Można jeszcze lepiej? Pewnie, dlaczego by nie?

3. Rozkładanie komponentów UI

Spójrzmy na nasz komponent, w którym pokazujemy dane użytkownika. W tym celu możemy stworzyć komponent UserDetails :

const UserDetails = (user) => {

    const showDetails = (user) => {
        alert(user.contact)
    }

    return <div key={user.id} onClick={() => showDetails(user)}>
        <div>{user.name}</div>
        <div>{user.email}</div>
    </div>
}


Ostatecznie, nasz oryginalny komponent staje się:

import React from "react";
import {useGetRemoteData} from "./useGetRemoteData";

export const Users = () => {
    const {filteredUsers , isLoading} = useGetRemoteData()

    return <>
        <div> Users List</div>
        <div> Loading state: {isLoading? 'Loading': 'Success'}</div>
        {filteredUsers.map(user => <UserDetails user={user}/>)}
    </>
}


Odchudziliśmy nasz kod z 60 linii do 12 linii! A do tego stworzyliśmy pięć oddzielnych komponentów, z których każdy ma jasno określoną i pojedynczą odpowiedzialność.

Przeanalizujmy, co właśnie zrobiliśmy. Przejrzyjmy nasze komponenty i sprawdźmy, czy udało nam się osiągnąć SRP:

  • User.js— odpowiedzialny za wyświetlanie listy użytkowników
  • UserDetails.js— odpowiedzialny za wyświetlanie danych użytkownika
  • useGetRemoteData.js— odpowiedzialny za filtrowanie zdalnych danych
  • useHttpGetrequest.js— odpowiada za wywołania HTTP
  • LoadingReducer.js— kompleksowe zarządzanie stanem


Oczywiście, możemy jeszcze poprawić wiele innych rzeczy, ale na ten moment powinno być to dla Ciebie dobrym punktem wyjścia.

Wnioski

Z tego artykułu dowiedzieliśmy się w jaki sposób, z pomocą zasad SOLID, zredukować ilość kodu w każdym pliku i stworzyć piękne i wielokrotnego użytku komponenty.

Oryginał tekstu w języku angielskim przeczytasz tutaj.

<p>Loading...</p>