Как стать автором
Обновить

Комментарии 18

Зачем здесь используется useContext? В официальной документации по этому хуку написано, что его следует использовать для хранения глобального состояния.

Контекст можно использовать и локально внутри компонентов, например чтобы не пропс-дриллить (как в данном случае).

Хотя, если автор хочет в оптимизацию, то можно было бы попробовать обернуть каждый Star в React.memo и передавать ему проп "isSelected", чтобы, например, при ховере обновлялись не все. Тогда контекст не нужен. Впрочем, эти оптимизации будут копеечные. А вот обернуть весь компонент в React.memo надо бы.

спасибо за комментарий, учту и дополню в статье

Все равно не очень понял, зачем тут контекст. Решить вопрос props drilling это понятно, но, что если компонент будет использоваться (а судя по уровню его абстракции, будет) на разных страницах, с разными данными, где-то рейтинг такой, где-то рейтинг другой. Каждый раз прокидывать новые данные не сломает результат? Или контекст всегда обновляется при монтировании того или иного компонента?

у каждого компонента свой контекст будет со своими конфигурациями

Интересная статья, как раз хотел с каким-нибудь js фреймворком поработать, возможно начну с Реакта.

<zanudamode>

React либа, а не фреймворк)

Чего ему не хватает чтобы называться фреймвоком? Каких характеристик или признаков?

Я, если честно, не понял зачем так сильно усложнять практически всё. Разве что ради того чтобы показать что ты умеешь в контексты. Только если контексты так использовать на каждый чих, то не выльется ли это в проблемы в итоге? Как по мне так в данном случае использование контекста - это misuse. Например компонент Star в каком-то другом месте использовать будет невозможно. Что совсем не круто.
Почему не используется TypeScript?
И, кстати, самое тормозное никак не оптимизировано - иконка. Представьте страницу на которой очень много звёздочек, а иконка каждой звездочки - отдельный svg элемент с одинаковым path. Это прямо очень тормозное дело, особенно если иконка в каждом блоке со компонентом рейтинга не одна. А ведь далеко не все иконки имеют такой короткий path. Я напоролся на такую проблему и скажу что абсолютно все остальные оптимизации - экономия на спичках. У меня в одном блоке было всего 4 разных иконки (разные, с длинным path), но блоков было много - от сотни до нескольких тысяч. В каждом блоке было от 4 до 12 иконок (чекбоксов могло быть от 1 до 8 в разном виде) Так вот - браузер очень туго переваривал весь этот зоопарк иконок. На довольно мощном компе.
Решение: в svg есть возможность клонирования/переиспользования элементов тэгом <use>.
Т.е. нужно:

  1. Вынести иконку в отдельный компонент

  2. При первом создании компонента пробрасывать полный svg в <body> или в другое место откуда будет максимально удобный доступ (Пример полного svg: <svg><circle cx="50" cy="50" r="10" fill="red" id="primcirc" /></svg>, обратите внимание на id). Тут особенность в том что нельзя чтобы полный SVG уничтожался при последующих перерисовках, иначе пропадут все иконки-клоны.

  3. При следующих рендерах компонента выводить не полный svg, а <use xlink:href="#primcirc" /> Подробнее гуглите: Cloning SVG Elements: Using use

Вот полный работающий код компонента иконки на основе MDIIcon (Material Design Community Icons):

import * as React from "react";
import {AllHTMLAttributes, CSSProperties, useEffect, useRef} from "react";

let idCounter: number = 0;
let reusableIcons: {
    [key: string]: boolean
} = {};

export interface IconProps extends Omit<AllHTMLAttributes<SVGSVGElement>, 'size' | 'label'> {
    id?: string,
    path: string,
    ref?: React.RefObject<SVGSVGElement>,
    title?: string,
    description?: string | null,
    horizontal?: boolean,
    vertical?: boolean,
    rotate?: number,
    spin?: boolean | number,
    style?: CSSProperties,
    size?: number | null,
    reuse?: string, //< icon id to be reused
    reusableItemsContainerJquerySelector?: string | 'body'
}

export default function MDIIcon(props: IconProps) {
    const {
        id,
        path,
        title,
        description,
        size,
        horizontal,
        vertical,
        rotate,
        spin,
        reuse,
        reusableItemsContainerJquerySelector,
        style: customStyle = {},
        ...rest
    } = props;

    const ref = useRef<SVGSVGElement>(null);

    useEffect(() => {
        if (reuse && ref.current && ref.current.getAttribute('data-use') === '0') {
            // copy original svg element to <body> so that it will persist there even after rerender
            // on rerender original svg element will be replaced by <svg><use href="#{reuse}"/></svg> and if
            // there are no clone inside <body> - all <use> tag will target nothing and icons will not be rerendered
            reusableIcons[reuse] = true;
            const existing = document.getElementById(reuse);
            if (existing) {
                window.$(existing).parent().remove();
            }
            const $container = window.$('<div style="display: none;" class="react-app"></div>')
                .append(window.$(ref.current.cloneNode(true) as any).attr('id', reuse));
            if (reusableItemsContainerJquerySelector === 'body') {
                window.$(document.body).append($container);
            } else {
                const $supercontainer = window.$(document.body).find(reusableItemsContainerJquerySelector as string);
                if ($supercontainer.length !== 0) {
                    $supercontainer.append($container);
                } else {
                    const message = '[MDIIcon] failed to find element for reusableItemsContainerJquerySelector = ' + reusableItemsContainerJquerySelector;
                    console.error(message);
                    window.$(document.body).append($container);
                }
            }
        }
    }, [ref.current, reuse])

    const index = ++idCounter;
    const pathStyle: any = {};
    const transform = [];
    const style = Object.assign({}, customStyle || {});
    if (size !== null) {
        style.height = style.width = size + 'px';
    }
    if (horizontal) {
        transform.push("scaleX(-1)");
    }
    if (vertical) {
        transform.push("scaleY(-1)");
    }
    if (rotate !== 0) {
        transform.push(`rotate(${rotate}deg)`);
    }
    let transformElement = (
        <path
            d={path}
            style={pathStyle}
        />
    );
    if (transform.length > 0) {
        style.transform = transform.join(' ');
        style.transformOrigin = 'center';
    }
    let spinElement = transformElement;
    const spinSec = spin || typeof spin !== 'number' ? 2 : spin;
    let inverse = horizontal || vertical;
    if (spinSec < 0) {
        inverse = !inverse
    }
    if (spin) {
        spinElement = (
            <g
                style={{
                    animation: `spin${inverse ? '-inverse' : ''} linear ${Math.abs(spinSec)}s infinite`,
                    transformOrigin: 'center'
                }}
            >
                {transformElement}
                {!(horizontal || vertical || rotate !== 0) && (
                    <rect
                        width="24"
                        height="24"
                        fill="transparent"
                    />
                )}
            </g>
        )
    }
    let ariaLabelledby;
    let labelledById = `icon_labelledby_${index}`;
    let describedById = `icon_describedby_${index}`;
    let role;
    if (title) {
        ariaLabelledby = description
            ? `${labelledById} ${describedById}`
            : labelledById;
    } else {
        role = 'presentation';
        if (description) {
            throw new Error("title attribute required when description is set");
        }
    }
    if (reuse && reusableIcons[reuse]) {
        return (
            <svg
                ref={ref}
                viewBox="0 0 24 24"
                style={style}
                role={role}
                aria-labelledby={ariaLabelledby}
                data-use="1"
                {...rest as any}
            >
                <use href={'#' + reuse}/>
            </svg>
        );
    } else {
        if (reuse) {
            reusableIcons[reuse] = true;
        }
        return (
            <svg
                ref={ref}
                viewBox="0 0 24 24"
                style={style}
                role={role}
                aria-labelledby={ariaLabelledby}
                id={id}
                data-use="0"
                {...rest as any}
            >
                {title && <title id={labelledById}>{title}</title>}
                {description && <desc id={describedById}>{description}</desc>}
                {spin && (
                    inverse
                        ? <style>{"@keyframes spin-inverse { to { transform: rotate(-360deg) } }"}</style>
                        : <style>{"@keyframes spin { to { transform: rotate(360deg) } }"}</style>
                )}
                {spinElement}
            </svg>
        );
    }
}

MDIIcon.defaultProps = {
    horizontal: false,
    vertical: false,
    rotate: 0,
    spin: false,
    size: 24,
    style: {},
    reusableItemsContainerJquerySelector: 'body'
} as Partial<IconProps>;

Извините за использование jQuery. Проект был на него завязан, а я не настолько ханжа чтобы этим не воспользоваться.
Я не претендую на идеальность кода или решения, но это единственный вариант который я нашел и который достаточно хорошо работает имея минимальные недостатки (по сути только лишний код в <body>). Не стоит пользоваться этой функцией везде - оно имеет смысл только если одинаковых иконок очень много (примерно от 50-100 штук на каждую иконку).

Задачей в данной статье не было реализовать npm пакет с данным виджетом для общего пользования, я хотел показать как решить данную задачу за минут 40 и какие требования могут быть. Ну и как оптимизировать можно.

Как ответили выше, контекст здесь ради того, чтоб не тянуть сторы в данный пример и избежать прокидывания пропсов, а показать реализацию на чистом React.

За замечание по SVG спасибо, очень полезно, попробую реализовать.

Давайте тогда представим что я тот кто оценивает Ваше решение =)
У меня возникают следующие вопросы:

  1. Почему использовался контекст? (Ответ уже знаем: чтобы не прокидывать пропсы)

  2. Чем Вас не устраивает прокидывание пропсов в данной задаче? Ведь это было бы проще и понятнее. Да и компонент же довольно простой, а не конструктор типа Dropdown Menu где контент передается извне и нужно иметь возможность взаимодействия с верхним уровнем.

  3. В проекте где есть рейтинг чаще всего будет и избранное, которое идеально ложится на функционал компонента Star, что будете делать?

  4. Почему не используется TypeScript? Он бы почти никак не повлиял на скорость решения задачи, зато была бы типизация хорошо прописана и соблюдена.

Посудите сами - данное тестовое задание дается не только чтобы проверить что Вы можете написать компонент или знаете какие-то особенности технологии/инструмента/языка, но и то как Вы это сделаете, какие возможности и как примените, и как Вы оцениваете ситуацию вне задачи (вопрос 3).
Я ничего не имею против контекстов, прекрасная штука, но тащить их только ради того чтобы не прокидывать пропсы - это как-то неэффективно ни по времени ни по результату. Более того контекст обязывает все компоненты его использующие быть зависимыми друг от друга. Уже нельзя будет для вопроса 3 взять готовое решение. Нужно будет переделывать. А это время и деньги. И не нужно говорить что этого небыло в задании. В реальности далеко не всегда заранее известно какой компонент будет переиспользоваться в будущем. Т.е. я как наниматель увидел что Вы смогли решили задачу, но не подумали о том что некоторые компоненты можно будет переиспользовать в будущем. Ведь у компонента Star очень широкое применение. Его и в like можно было бы превратить всего-лишь поменяв иконку. Но увы, придется делать отдельный компонент...

У вас хорошо получается раскрыть тему, но будем реалистами, на интервью даже такого решение врятли будет, там все будет в одном js файле, а не разбито по папочкам и вылизано. Иногда приходилось ещё и подключать все самому, потому что было просто ничего в редакторе :)

Для меня на TypeScript заняло бы больше времени на описание типов и интерфейсов, но если вы успеете на лету это все продумать и описать, то почему нет)

Я как то больше сфокусировался на требованиях и простой реализации, что забыл о моменте переиспользования в статье, это важный момент, в этом случае архитектура будет совершенно другой и без контекстов.

P.S. судя по количеству комментариев и их размеру вам нужно писать статьи )

Для меня на TypeScript заняло бы больше времени на описание типов и интерфейсов

Смотря где пишется код. В блокноте - само собой, а в вебшторме за счёт более лучшего автокомплита и оперативной проверки опечаток могло быть и быстрее.

Глючный codesandbox или ещё лучше whiteboard, речи об IDE даже не идет)

Понятно в рабочей обстановке TypeScript, IDE, автокомплит и гугл лучшие друзья :)

Как по мне, то если на собеседовании загоняют в такие рамки и не дают использовать привычный или хотябы схожий набор инструментов, то возникает вопрос об адекватности собеседующих. Отнимите в ответ у собеседующего бумажку/планшет/ноут по которым они вопросы задают. Пусть почувствуют себя на Вашем месте =)))

К сожалению таковы правила игры :)

Мне лень статьи писать =) Я даже комментирую редко.

Сейчас я поведаю магию. А то привыкли все в дивы пихать. Берём radio button, и называем нашу группу star. Там дам, у нас открывается и нативное value, и скрин ридером можно пройтись. А велосипед всегда можно придумать.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории