Sytuacja kobiet w IT w 2024 roku
27.04.20216 min
Iskander Samatov

Iskander SamatovSenior Software EngineerHubSpot

Jak pisać prostsze komponenty w React - 9 wskazówek

Poznaj 9 metod, które sprawią, że Twoje komponenty w React będą prostsze i bardziej przejrzyste.

Jak pisać prostsze komponenty w React - 9 wskazówek

W artykule tym przeanalizujemy kilka prostych wskazówek, które pomogą Ci pisać prostsze i bardziej przejrzyste komponenty w React oraz lepiej skalować projekty. 

Nie przekazuj właściwości przy pomocy operatora spread

Zacznijmy od antywzorca, którego należy unikać. Jeśli nie ma takiej potrzeby, staraj się unikać przekazywania właściwości w drzewie komponentów przy użyciu operatora spread, takiego jak {...props}.

Przekazywanie właściwości w taki właśnie sposób sprawia, że komponenty pisze się szybciej, ale ciężej jest też wyłapać błędy w kodzie. Tracisz w ten sposób pewność, czy w ogóle działają, co sprawia, że trudniej jest je refaktoryzować - błędy pojawią się wtedy prędzej, czy później. 

Opakuj parametry funkcji w obiekt

Jeśli Twoja funkcja akceptuje kilka parametrów, dobrym pomysłem będzie opakowanie ich w obiekt. Oto przykład:

export const sampleFunction = ({ param1, param2, param3 }) => {
    console.log({ param1, param2, param3 });
}


Pisanie sygnatury funkcji w taki właśnie sposób daje nam takie oto korzyści:

  1. Nie musisz się już martwić kolejnością przekazywania argumentów.Kilka razy popełniłem jednak ten błąd - wprowadziłem do kodu błąd i przekazałem argumenty funkcji w złej kolejności
  2. W edytorach ze skonfigurowanym IntelliSense (większość z nich już to posiada) mamy funkcję autouzupełniania, jeśli chodzi o pisanie argumentów funkcji.

Do obsługi zdarzeń używaj funkcji, które zwracają funkcję obsługującą

Jeśli wiesz coś o programowaniu funkcyjnym, to zorientujesz się pewnie, że technika ta przypomina currying - bo ustawiasz niektóre parametry przed czasem. 

Spójrzmy na poniższy przykład:

import React from 'react'

export default function SampleComponent({ onValueChange }) {

    const handleChange = (key) => {
        return (e) => onValueChange(key, e.target.value)
    }


    return (
        <form>
            <input onChange={handleChange('name')} />
            <input onChange={handleChange('email')} />
            <input onChange={handleChange('phone')} />
        </form>
    )
}


Pisząc funkcję obsługującą w taki właśnie sposób, sprawiamy, że drzewo komponentów staje się bardziej przejrzyste. 

Używaj maps zamiast if/else

Gdy musisz wyrenderować różne elementy w oparciu o niestandardową logikę, to polecam korzystać z instrukcji maps, a nie if/else

Oto przykład użycia if/else:

import React from 'react'

const Student = ({ name }) => <p>Student name: {name}</p>
const Teacher = ({ name }) => <p>Teacher name: {name}</p>
const Guardian = ({ name }) => <p>Guardian name: {name}</p>


export default function SampleComponent({ user }) {
    let Component = Student;
    if (user.type === 'teacher') {
        Component = Teacher
    } else if (user.type === 'guardian') {
        Component = Guardian
    }

    return (
        <div>
            <Component name={user.name} />
        </div>
    )
}


A oto przykład użycia maps:

import React from 'react'

const Student = ({ name }) => <p>Student name: {name}</p>
const Teacher = ({ name }) => <p>Teacher name: {name}</p>
const Guardian = ({ name }) => <p>Guardian name: {name}</p>

const COMPONENT_MAP = {
    student: Student,
    teacher: Teacher,
    guardian: Guardian
}

export default function SampleComponent({ user }) {
    const Component = COMPONENT_MAP[user.type]

    return (
        <div>
            <Component name={user.name} />
        </div>
    )
}


Dzięki tej prostej metodzie Twoje komponenty stają się bardziej deklaratywne i łatwiejsze do zrozumienia. Rozszerzanie logiki i dodawanie do niej kolejnych elementów też jest wtedy prostsze.

Komponenty Hook

Ten wzorzec jest bardzo przydatny, ale tylko jeśli się go nie nadużywa. 

Czasem zdarzy się, że niektórych komponentów będziesz używać w swojej aplikacji praktycznie wszędzie. Jeśli potrzebują stanu, aby działać, to możesz opakować je w hook, który ten stan zapewnia. Dobrymi przykładami takich komponentów są popupy, powiadomienia typu toast, lub proste modale - oto przykład takiego modala:

import React, { useCallback, useState } from 'react';
import ConfirmationDialog from 'components/global/ConfirmationDialog';

export default function useConfirmationDialog({
    headerText,
    bodyText,
    confirmationButtonText,
    onConfirmClick,
}) {
    const [isOpen, setIsOpen] = useState(false);

    const onOpen = () => {
        setIsOpen(true);
    };

    const Dialog = useCallback(
        () => (
            <ConfirmationDialog
                headerText={headerText}
                bodyText={bodyText}
                isOpen={isOpen}
                onConfirmClick={onConfirmClick}
                onCancelClick={() => setIsOpen(false)}
                confirmationButtonText={confirmationButtonText}
            />
        ),
        [isOpen]
    );

    return {
        Dialog,
        onOpen,
    };
}


Potem możesz już skorzystać z komponentu hook w następujący sposób:

import React from "react";
import { useConfirmationDialog } from './useConfirmationDialog'

function Client() {
  const { Dialog, onOpen } = useConfirmationDialog({
    headerText: "Delete this record?",
    bodyText:
      "Are you sure you want delete this record? This cannot be undone.",
    confirmationButtonText: "Delete",
    onConfirmClick: handleDeleteConfirm,
  });

  function handleDeleteConfirm() {
    //TODO: delete
  }

  const handleDeleteClick = () => {
    onOpen();
  };

  return (
    <div>
      <Dialog />
      <button onClick={handleDeleteClick} />
    </div>
  );
}

export default Client;


Takie zapisanie komponentu sprawia, że nie trzeba pisać masy kodu związanego z utrzymaniem stanu. Jeśli chcesz dowiedzieć się więcej o React hooks, sprawdź mój poprzedni artykuł

Dzielenie komponentów

Następne 3 wskazówki dotyczą inteligentnego dzielenia komponentów. Z własnego doświadczenia mogę powiedzieć, że małe komponenty to najlepszy sposób na to, aby Twój projekt dało się jakoś utrzymać. 

Korzystaj z wrapperów

Jeśli masz problem ze znalezieniem sposobu na podzielenie komponentów, spójrz na to, jakie funkcje daje nam każdy z nich. Niektóre elementy są po to, aby zapewniać różniące się od siebie funkcje - np. handlery drag i drop

Oto przykład komponentu, który implementuje drag-and-drop przy użyciu react-beautiful-dnd:

import React from 'react'
import { DragDropContext, Droppable } from 'react-beautiful-dnd';



export default function DraggableSample() {

    function handleDragStart(result) {
        console.log({ result });
    }

    function handleDragUpdate({ destination }) {
        console.log({ destination });
    }

    const handleDragEnd = ({ source, destination }) => {
        console.log({ source, destination });
    };

    return (
        <div>
            <DragDropContext
                onDragEnd={handleDragEnd}
                onDragStart={handleDragStart}
                onDragUpdate={handleDragUpdate}
            >
                <Droppable droppableId="droppable" direction="horizontal">
                    {(provided) => (
                        <div {...provided.droppableProps} ref={provided.innerRef}>
                            {columns.map((column, index) => {
                                return (
                                    <ColumnComponent
                                        key={index}
                                        column={column}
                                    />
                                );
                            })}
                        </div>
                    )}
                </Droppable>
            </DragDropContext>
        </div>
    )
}


Zobacz, jak komponent ten wygląda po przeniesieniu logiki drag-and-drop do wrappera:

import React from 'react'

export default function DraggableSample() {
    return (
        <div>
            <DragWrapper>
                {columns.map((column, index) => {
                    return (
                        <ColumnComponent
                            key={index}
                            column={column}
                        />
                    );
                })}
            </DragWrapper>
        </div>
    )
}


A oto kod wrappera:

import React from 'react'
import { DragDropContext, Droppable } from 'react-beautiful-dnd';

export default function DragWrapper({children}) {

    function handleDragStart(result) {
        console.log({ result });
    }

    function handleDragUpdate({ destination }) {
        console.log({ destination });
    }

    const handleDragEnd = ({ source, destination }) => {
        console.log({ source, destination });
    };

    return (
        <DragDropContext
            onDragEnd={handleDragEnd}
            onDragStart={handleDragStart}
            onDragUpdate={handleDragUpdate}
        >
            <Droppable droppableId="droppable" direction="horizontal">
                {(provided) => (
                    <div {...provided.droppableProps} ref={provided.innerRef}>
                        {children}
                    </div>
                )}
            </Droppable>
        </DragDropContext>
    )
}


W rezultacie łatwiej jest patrzeć na dany komponent i można szybciej zrozumieć, co robi na wysokim poziomie. Wszystkie funkcje drag-and-drop są wtedy we wrapperze i łatwiej jest o nich myśleć. 

Zasada Separation of Concerns

Jest to mój ulubiony sposób na dzielenie większych komponentów. 

W kontekście Reacta zasada Separation of Concerns oznacza separowanie części komponentów, które odpowiadają za pobieranie i mutowanie danych i tych, które odpowiadają za wyświetlanie drzewa elementów. 

Metoda ta jest głównym powodem, dla którego wprowadzono wzorzec hook. Logikę zarządzającą wywołaniami API powinno się opakować w niestandardowy hook. 

Spójrzmy na następujący komponent: 

import React from 'react'
import { someAPICall } from './API'
import ItemDisplay from './ItemDisplay'



export default function SampleComponent() {
    const [data, setData] = useState([])

    useEffect(() => {
        someAPICall().then((result) => {
            setData(result)
        })
    }, [])

    function handleDelete() {
        console.log('Delete!');
    }

    function handleAdd() {
        console.log('Add!');
    }

    const handleEdit = () => {
        console.log('Edit!');
    };

    return (
        <div>
            <div>
                {data.map(item => <ItemDisplay item={item} />)}
            </div>
            <div>
                <button onClick={handleDelete} />
                <button onClick={handleAdd} />
                <button onClick={handleEdit} />
            </div>
        </div>
    )
}


A oto wersja po refactoringu z podziałem kodu za pomocą niestandardowych hooków: 

import React from 'react'
import ItemDisplay from './ItemDisplay'

export default function SampleComponent() {
    const { data, handleDelete, handleEdit, handleAdd } = useCustomHook()

    return (
        <div>
            <div>
                {data.map(item => <ItemDisplay item={item} />)}
            </div>
            <div>
                <button onClick={handleDelete} />
                <button onClick={handleAdd} />
                <button onClick={handleEdit} />
            </div>
        </div>
    )
}


A oto sam hook:

import { someAPICall } from './API'


export const useCustomHook = () => {
    const [data, setData] = useState([])

    useEffect(() => {
        someAPICall().then((result) => {
            setData(result)
        })
    }, [])

    function handleDelete() {
        console.log('Delete!');
    }

    function handleAdd() {
        console.log('Add!');
    }

    const handleEdit = () => {
        console.log('Edit!');
    };

    return { handleEdit, handleAdd, handleDelete, data }
}

Osobny plik dla każdego komponentu

Ludzie często piszą taki właśnie kod:

import React from 'react'



export default function SampleComponent({ data }) {

    const ItemDisplay = ({ name, date }) => (
        <div>
            <h3>{name}</h3>
            <p>{date}</p>
        </div>
    )
    
    return (
        <div>
            <div>
                {data.map(item => <ItemDisplay item={item} />)}
            </div>

        </div>
    )
}


I pomimo że pisanie komponentów Reacta w taki sposób nie jest jakoś szczególnie złe, to niestety nie jest to zbyt dobra praktyka. Nie ma żadnych minusów przenoszenia ItemDisplay do oddzielnego pliku. Plusem jest natomiast to, że Twoje komponenty są luźno powiązane i łatwiejsze do rozszerzenia. 

Pisanie prostego kodu sprowadza się często do uważnego pisania i trzymania się dobrych wzorców i unikania antywzorców - zdecydowanie pomoże to w pisaniu prostszych komponentów w React. Powyższe wzorce są dla mnie bardzo przydatne - mam nadzieję, że przydadzą się i Tobie. 


Oryginał tekstu w języku angielskim możesz przeczytać tutaj

<p>Loading...</p>