React.js. Компоненты высшего порядка

12.09.2021

Теги: FrontendJavaScriptReact.jsWeb-разработкаКлассКомпонентТеорияФункция

Компонент высшего порядка в React это паттерн, используемый для того, чтобы делить функционал между компонентами без повторения кода. Такие компоненты, по факту, не совсем являются компонентами, это скорее функции. Такая функция берёт компонент как аргумент и возвращает новый компонент. Она переделывает исходный компонент в новый компонент, добавляяя в него данные и/или фунционал.

Добавляем в классовый компонент Example данные и функционал:

import Example from './Example';
import WithData from './WithData';

export default function App() {
    const ExampleWithData = WithData(Example);
    return <ExampleWithData />
}
const items = ['one', 'two'];
const hello = () => alert('Hello!');

export default function WithData(Component) {
    return class extends React.Component {
        render() {
            return <Component items={items} hello={hello} {...this.props} />;
        }
    }
}
export default class Example extends React.Component {
    render() {
        return (
            <>
                <h1>Component «Example»</h1>
                {this.props.items ? (
                    <ul>
                        {this.props.items.map((item, index) => <li key={index}>{item}</li>)}
                    </ul>
                ) : (
                    <p>No data</p>
                )} 
                {this.props.hello && <button onClick={this.props.hello}>Say hello</button>}
            </>
        );
    }
}

Добавляем в функциональный компонент Example данные и функционал:

import Example from './Example';
import WithData from './WithData';

export default function App() {
    const ExampleWithData = WithData(Example);
    return <ExampleWithData />
}
const items = ['one', 'two'];
const hello = () => alert('Hello!');

export default function WithData(Component) {
    return (props) => <Component items={items} hello={hello} {...props} />
}
export default function Example(props) {
    return (
        <>
            <h1>Component «Example»</h1>
            {props.items ? (
                <ul>
                    {props.items.map((item, index) => <li key={index}>{item}</li>)}
                </ul>
            ) : (
                <p>No data</p>
            )} 
            {props.hello && <button onClick={props.hello}>Say hello</button>}
        </>
    );
}

Пример использования

Допустим, у нас есть новостной сайт, на главной странице которого показывается список новостей. И есть панель управления, где тоже показывается список новостей, но с элементами управления. Компоненты NewsListForUser и NewsListForAdmin, которые отвечают за показ списка, нуждаются в функционале получения новостей с сервера. Было бы логично вынести этот функцонал куда-то за пределы этих двух компонентов, чтобы не дублировать код.

import { useState } from 'react';
import NewsListForUser from './NewsListForUser';
import NewsListForAdmin from './NewsListForAdmin';
import WithData from './WithData';

export default function News() {
    const [admin, setAdmin] = useState(false);

    let NewsList;
    if (admin) {
        NewsList = NewsListForAdmin;
    } else {
        NewsList = NewsListForUser;
    }
    const NewsListWithData = WithData(NewsList);

    const toggle = () => setAdmin(!admin);

    return (
        <>
            <NewsListWithData heading="Новости" />
            <button onClick={toggle}>Переключить на {admin ? 'user' : 'admin'}</button>
        </>
    )
}

Компонент высшего порядка WithData, который не совсем компонент, а функция высшего порядка — получает список новостей с сервера и хранит их в состоянии.

import React from 'react';

export default function WithData(Component) {
    return class extends React.Component {
        state = {
            loading: true,
            items: [],
        }

        componentDidMount() {
            fetch('https://jsonplaceholder.typicode.com/posts')
                .then(response => response.json())
                .then(json => {
                    this.setState({
                        items: json.splice(0, 3),
                        loading: false
                    });
                });
        }

        render() {
            return <Component loading={this.state.loading} items={this.state.items} {...this.props} />;
        }
    }
}

Компоненты NewsListForUser и NewsListForAdmin отвечают за показ списка новостей для обычного пользователя и для администратора сайта:

export default function NewsListForUser(props) {
    if (props.loading) {
        return <p>Loading...</p>;
    }

    return (
        <div>
            <h2>{props.heading}</h2>
            {props.items.length ? (
                props.items.map(item => 
                    <div key={item.id}>
                        <h4>{item.title}</h4>
                        <p>{item.body}</p>
                    </div>
                )
            ) : (
                <p>No news</p>
            )}
        </div>
    );
}
export default function NewsListForAdmin(props) {
    if (props.loading) {
        return <p>Loading...</p>;
    }

    return (
        <div>
            <h2>{props.heading}</h2>
            {props.items.length ? (
                props.items.map(item => 
                    <div key={item.id}>
                        <h4>{item.title}</h4>
                        <p>{item.body}</p>
                        <button>Редактировать</button>
                        <button>Удалить</button>
                    </div>
                )
            ) : (
                <p>No news</p>
            )}
        </div>
    );
}

Хуки вместо HOC

HOC активно использовались c классовыми компонентами, с появлением хуков ситуация изменилась — теперь пример выше можно переделать.

import { useState } from 'react';
import NewsListForUser from './NewsListForUser';
import NewsListForAdmin from './NewsListForAdmin';
import useFetchData from './useFetchData';

export default function News() {
    const [admin, setAdmin] = useState(false);
    const { loading, items, error } = useFetchData();

    const toggle = () => setAdmin(!admin);

    if (loading) {
        return <p>Loading data...</p>;
    }

    if (error) {
        return <p>Something wrong</p>;
    }

    return (
        <>
            {admin ? (
                <NewsListForAdmin heading="Новости" items={items} />
            ) : (
                <NewsListForUser heading="Новости" items={items} />
            )}
            <button onClick={toggle}>Переключить на {admin ? 'user' : 'admin'}</button>
        </>
    )
}
import { useState, useEffect } from 'react';

export default function useFetchData() {
    const [loading, setLoading] = useState(true);
    const [items, setItems] = useState([]);
    const [error, setError] = useState(false);

    useEffect(() => {
        async function fetchData() {
            try {
                const data = await fetch('https://jsonplaceholder.typicode.com/posts');
                const json = await data.json();
                setItems(json.splice(0, 3));
            } catch (error) {
                setError(true);
            }
            setLoading(false);
        }
        fetchData();
    }, []);

    return {
        loading,
        items,
        error
    };
}
export default function NewsListForUser(props) {
    return (
        <div>
            <h2>{props.heading}</h2>
            {props.items.length ? (
                props.items.map(item => 
                    <div key={item.id}>
                        <h4>{item.title}</h4>
                        <p>{item.body}</p>
                    </div>
                )
            ) : (
                <p>No news</p>
            )}
        </div>
    );
}
export default function NewsListForAdmin(props) {
    return (
        <div>
            <h2>{props.heading}</h2>
            {props.items.length ? (
                props.items.map(item => 
                    <div key={item.id}>
                        <h4>{item.title}</h4>
                        <p>{item.body}</p>
                        <button>Редактировать</button>
                        <button>Удалить</button>
                    </div>
                )
            ) : (
                <p>No news</p>
            )}
        </div>
    );
}

Вместо заключения

Функции в JavaScript являются объектами первого класса. Это означает, что их, как и объекты, массивы или строки, можно присваивать переменным, передавать функциям в виде аргументов или возвращать их из других функций.

function add(x, y) {
    return x + y
}

function addFive(x, callback) {
  return callback(x, 5)
}

addFive(10, add) // 15

Мы передаём функцию add функции addFive в качестве аргумента, там ее вызываем. Функция, переданная другой в виде аргумента, называется коллбэком (функцией обратного вызова), а функция, которая получает другую функцию в виде аргумента, называется функцией высшего порядка.

Поиск: JavaScript • React.js • Web-разработка • Frontend • Класс • Теория • Функция • Компонент • HOC

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.