Vytváření vysoce znovupoužitelných komponent React.js pomocí složeného vzoru

Dnes vám přináším způsob, jak vytvořit vysoce opakovaně použitelnou komponentu React pomocí pokročilého vzoru zvaného Compound .

Vzor složených komponent

Klíčové slovo v názvu vzoru je slovo Složený , slovo sloučenina označuje něco, co se skládá ze dvou nebo více samostatných prvků.

S ohledem na složky React by to mohlo znamenat složku, která se skládá ze dvou nebo více samostatných složek. Hlavní složka se obvykle nazývá rodič a samostatné složené složky, děti .

Podívejte se na následující příklad:

Zde <Select> je rodič komponentu a <Select.Option> jsou dětské komponenty

Celkové chování prvku select také závisí na tom, že tyto prvky jsou také složeny. Jsou tedy vzájemně propojeny.

Stát celé komponenty je spravováno Select komponenta se všemi Select.Option podřízené komponenty závislé na tomto stavu.

Máte představu o tom, jaké složené složky jsou nyní?

Chystáme se vytvořit Select komponentu, kterou jsme viděli výše, která se bude skládat ze 2 dalších komponent Select Dropdown a Select Option .


V bloku kódu výše si všimnete, že jsem použil výrazy jako tento:Select.Option

Můžete to udělat také:

Oba fungují, ale je to věc osobních preferencí. Podle mého názoru dobře komunikuje závislost hlavní komponenty, ale to je jen moje preference.

Vytváření složených podřízených komponent

Select je naše hlavní součást, bude sledovat stav a bude to dělat prostřednictvím booleovské proměnné nazvané visible .

// select state 
{
  visible: true || false
}

Select komponenta potřebuje sdělit stav každé podřízené komponentě bez ohledu na jejich pozici ve stromu vnořených komponent.

Pamatujte, že podřízené položky jsou závislé na nadřazené složené složce pro daný stav.

Jaký by byl nejlepší způsob, jak to udělat?

Potřebujeme použít React Context API k udržení stavu komponenty a odhalení viditelného prostřednictvím Poskytovatele komponent. Vedle viditelného vlastnost, vystavíme také řetězec prop, který bude obsahovat vybranou možnost value .

Vytvoříme to v souboru s názvem select-context.js

import { createContext, useContext } from 'react'

const defaultContext = {
  visible: false,
  value: ''
};

export const SelectContext = createContext(defaultContext);

export const useSelectContext = () => useContext(SelectContext);

Nyní musíme vytvořit soubor s názvem select-dropdown.js což je kontejner pro vybrané možnosti.

import React from "react";
import PropTypes from "prop-types";
import { StyledDropdown } from "./styles";

const SelectDropdown = ({ visible, children, className = "" }) => {
  return (
    <StyledDropdown visible={visible} className={className}>
      {children}
    </StyledDropdown>
  );
};

SelectDropdown.propTypes = {
  children: PropTypes.oneOfType([
    PropTypes.arrayOf(PropTypes.node),
    PropTypes.node
  ]),
  visible: PropTypes.bool.isRequired,
  className: PropTypes.string
};

export default SelectDropdown;

Dále musíme vytvořit soubor s názvem styles.js pro uložení stylů komponent.

import styled, { css } from "styled-components";

export const StyledDropdown = styled.div`
  position: absolute;
  border-radius: 1.375rem;
  box-shadow: 0 5px 10px rgba(0, 0, 0, 0.12);
  background-color: #fff;
  max-height: 15rem;
  width: 80vw;
  overflow-y: auto;
  overflow-anchor: none;
  padding: 1rem 0;
  opacity: ${(props) => (props.visible ? 1 : 0)};
  visibility: ${(props) => (props.visible ? "visible" : "hidden")};
  top: 70px;
  left: 10px;
  z-index: 1100;
  transition: opacity 0.2s, transform 0.2s, bottom 0.2s ease,
    -webkit-transform 0.2s;
`;

Poté musíme vytvořit podřízenou komponentu, k tomu vytvoříme soubor s názvem select-option.js .

import React, { useMemo } from "react";
import { useSelectContext } from "./select-context";
import { StyledOption } from "./styles";
import PropTypes from "prop-types";


const SelectOption = ({
  children,
  value: identValue,
  className = "",
  disabled = false
}) => {
  const { updateValue, value, disableAll } = useSelectContext();

  const isDisabled = useMemo(() => disabled || disableAll, [
    disabled,
    disableAll
  ]);

  const selected = useMemo(() => {
    if (!value) return false;
    if (typeof value === "string") {
      return identValue === value;
    }
  }, [identValue, value]);

  const bgColor = useMemo(() => {
    if (isDisabled) return "#f0eef1";
    return selected ? "#3378F7" : "#fff";
  }, [selected, isDisabled]);

  const hoverBgColor = useMemo(() => {
    if (isDisabled || selected) return bgColor;
    return "#f0eef1";
  }, [selected, isDisabled, bgColor]);

  const color = useMemo(() => {
    if (isDisabled) return "#888888";
    return selected ? "#fff" : "#888888";
  }, [selected, isDisabled]);

  const handleClick = (event) => {
    event.preventDefault();
    if (typeof updateValue === "function" && identValue !== value) {
      updateValue(identValue);
    }
  };

  return (
    <StyledOption
      className={className}
      bgColor={bgColor}
      hoverBgColor={hoverBgColor}
      color={color}
      idDisabled={disabled}
      disabled={disabled}
      onClick={handleClick}
    >
      {children}
    </StyledOption>
  );
};

SelectOption.propTypes = {
  children: PropTypes.oneOfType([
    PropTypes.arrayOf(PropTypes.node),
    PropTypes.node
  ]),
  value: PropTypes.string,
  className: PropTypes.string,
  disabled: PropTypes.boolean
};

export default SelectOption;

Vím, že je to zmatené, ale rozeberu to.

Nejprve se zaměřme na následující řádek kódu:

 const { updateValue, value, disableAll } = useSelectContext();

Používáme useSelectContext() od select-context.js pro přístup ke kontextovým datům, "⚠️Spoiler alert":tato data budeme spravovat na naší hlavní komponentě, Ano, máte pravdu, je Select komponenta.

value vrtule z context je vybraná hodnota.

Také používáme useMemo několikrát, aby se předešlo zbytečným renderům.

  const bgColor = useMemo(() => {
    if (isDisabled) return "#f0eef1";
    return selected ? "#3378F7" : "#fff";
  }, [selected, isDisabled]);

useMemo provede zpětné volání, které vrátí string hodnotu s hexadecimálním kódem barvy a předáme závislost pole [selected, isDisabled]. To znamená, že zapamatovaná hodnota zůstane stejná, pokud se nezmění závislosti.

Nejste si jisti, jak useMemo funguje? Podívejte se na tento cheatsheet.

Nyní dokončit SelectOption potřebujeme vytvořit StyledOption komponenta pro to přejdeme na styles.js soubor a napište následující kód:

export const StyledOption = styled.div`
  display: flex;
  max-width: 100%;
  justify-content: flex-start;
  align-items: center;
  font-weight: normal;
  font-size: 1.3rem;
  height: 4rem;
  padding: 0 2rem;
  background-color: ${(props) => props.bgColor};
  color: ${(props) => props.color};
  user-select: none;
  border: 0;
  cursor: ${(props) => (props.isDisabled ? "not-allowed" : "pointer")};
  transition: background 0.2s ease 0s, border-color 0.2s ease 0s;
  &:hover {
    background-color: ${(props) => props.hoverBgColor};
  }
`;

Vytvoření hlavní komponenty

Do této chvíle máme všechny podřízené komponenty naší hlavní komponenty, nyní vytvoříme hlavní komponentu Select , k tomu potřebujeme vytvořit soubor s názvem select.js s následujícím kódem:

import React, { useState, useCallback, useMemo, useEffect } from "react";
import { SelectContext } from "./select-context";
import { StyledSelect, StyledValue, StyledIcon, TruncatedText } from "./styles";
import SelectDropdown from "./select-dropdown";
import { pickChildByProps } from "../../utils";
import { ChevronDown } from "react-iconly";
import PropTypes from "prop-types";

const Select = ({
  children,
  value: customValue,
  disabled = false,
  onChange,
  icon: Icon = ChevronDown,
  className,
  placeholder = "Choose one"
}) => {
  const [visible, setVisible] = useState(false);
  const [value, setValue] = useState(undefined);

  useEffect(() => {
    if (customValue === undefined) return;
    setValue(customValue);
  }, [customValue]);

  const updateVisible = useCallback((next) => {
    setVisible(next);
  }, []);

  const updateValue = useCallback(
    (next) => {
      setValue(next);
      if (typeof onChange === "function") {
        onChange(next);
      }
      setVisible(false);
    },
    [onChange]
  );

  const clickHandler = (event) => {
    event.preventDefault();
    if (disabled) return;
    setVisible(!visible);
  };

  const initialValue = useMemo(
    () => ({
      value,
      visible,
      updateValue,
      updateVisible,
      disableAll: disabled
    }),
    [visible, updateVisible, updateValue, disabled, value]
  );

  const selectedChild = useMemo(() => {
    const [, optionChildren] = pickChildByProps(children, "value", value);
    return React.Children.map(optionChildren, (child) => {
      if (!React.isValidElement(child)) return null;
      const el = React.cloneElement(child, { preventAllEvents: true });
      return el;
    });
  }, [value, children]);

  return (
    <SelectContext.Provider value={initialValue}>
      <StyledSelect
        disabled={disabled}
        className={className}
        onClick={clickHandler}
      >
        <StyledValue isPlaceholder={!value}>
          <TruncatedText height="4rem">
            {!value ? placeholder : selectedChild}
          </TruncatedText>
        </StyledValue>
        <StyledIcon visible={visible}>
          <Icon />
        </StyledIcon>
        <SelectDropdown visible={visible}>{children}</SelectDropdown>
      </StyledSelect>
    </SelectContext.Provider>
  );
};

Select.propTypes = {
  children: PropTypes.oneOfType([
    PropTypes.arrayOf(PropTypes.node),
    PropTypes.node
  ]),
  disabled: PropTypes.bool,
  icon: PropTypes.element,
  value: PropTypes.string,
  placeholder: PropTypes.string,
  onChange: PropTypes.func,
  className: PropTypes.string
};

export default Select;

Začnu vysvětlením propTypes:

  • children :Jsou pole Select.Option
  • disabled :Používá se k nastavení deaktivovaného stavu v Select a Select.Option
  • value :Je výchozí vybraná hodnota
  • placeholder :Používá se k zobrazení textu, pokud nejsou žádné Select.Option vybráno.
  • onChange :Zpětné volání pro komunikaci při změně hodnoty
  • className :Název třídy pro Select komponent

Perfektní, nyní se zaměřme na useState React hook, používá se ke správě stavu vybrané hodnoty a viditelnosti rozbalovací nabídky

  const [visible, setVisible] = useState(false);
  const [value, setValue] = useState(undefined);

Chcete-li nastavit počáteční hodnotu Select (v případě, že je nastaven jeden), musíme použít háček useEffect

  useEffect(() => {
    if (customValue === undefined) return;
    setValue(customValue);
  }, [customValue]);

  const updateVisible = useCallback((next) => {
    setVisible(next);
  }, []);

  const updateValue = useCallback(
    (next) => {
      setValue(next);
      if (typeof onChange === "function") {
        onChange(next);
      }
      setVisible(false);
    },
    [onChange]
  );

Další háčky, které používáme, je useCallback , tento hák vrátí zapamatovanou verzi zpětného volání, která se změní pouze v případě, že se změnila jedna ze závislostí. To je užitečné při předávání zpětných volání optimalizovaným podřízeným komponentám, které se spoléhají na referenční rovnost, aby se zabránilo zbytečnému vykreslování (např. shouldComponentUpdate).

useCallback(fn, deps) je ekvivalentní useMemo(() => fn, deps).

Nyní se zaměříme na počáteční hodnotu kontextu, podívejme se na následující kód:

  const initialValue = useMemo(
    () => ({
      value,
      visible,
      updateValue,
      updateVisible,
      disableAll: disabled
    }),
    [visible, updateVisible, updateValue, disabled, value]
  );

return (
    <SelectContext.Provider value={initialValue}>
     // ---- ///
    </SelectContext.Provider>
  );

Ve výše uvedeném kódu používáme useMemo abychom zabránili zbytečným opakovaným vykreslování předávání v poli rekvizity, které se mohou změnit, předáme tuto počáteční hodnotu do SelectContect.Provider , používáme každou z těchto vlastností v komponentách, které jsme viděli dříve.

V neposlední řadě máme funkci pro získání vybrané komponenty volby, podívejme se na následující kód:

export const pickChildByProps = (children, key, value) => {
  const target = [];
  const withoutPropChildren = React.Children.map(children, (item) => {
    if (!React.isValidElement(item)) return null;
    if (!item.props) return item;
    if (item.props[key] === value) {
      target.push(item);
      return null;
    }
    return item;
  });

  const targetChildren = target.length >= 0 ? target : undefined;

  return [withoutPropChildren, targetChildren];
};

 const selectedChild = useMemo(() => {
    const [, optionChildren] = pickChildByProps(children, "value", value);
    return React.Children.map(optionChildren, (child) => {
      if (!React.isValidElement(child)) return null;
      const el = React.cloneElement(child, { preventAllEvents: true });
      return el;
    });
  }, [value, children]);

Stručně řečeno, naklonujeme vybranou možnost a vložíme ji do záhlaví Select komponenta.

Nyní musíme vytvořit potřebné styly pro Select komponent:

export const StyledSelect = styled.div`
  position: relative;
  z-index: 100;
  display: inline-flex;
  align-items: center;
  user-select: none;
  white-space: nowrap;
  cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
  width: 80vw;
  transition: border 0.2s ease 0s, color 0.2s ease-out 0s,
    box-shadow 0.2s ease 0s;
  box-shadow: 0 5px 10px rgba(0, 0, 0, 0.12);
  border: 2px solid #f5f5f5;
  border-radius: 3rem;
  height: 4rem;
  padding: 0 1rem 0 1rem;
  background-color: ${(props) => (props.disabled ? "#f0eef1" : "#fff")};
  &:hover {
    border-color: ${(props) => (props.disabled ? "#888888" : "#3378F7")};
  }
`;

export const StyledIcon = styled.div`
  position: absolute;
  right: 2rem;
  font-size: ${(props) => props.size};
  top: 50%;
  bottom: 0;
  transform: translateY(-50%)
    rotate(${(props) => (props.visible ? "180" : "0")}deg);
  pointer-events: none;
  transition: transform 200ms ease;
  display: flex;
  align-items: center;
  color: #999999;
`;

export const StyledValue = styled.div`
  display: inline-flex;
  flex: 1;
  height: 100%;
  align-items: center;
  line-height: 1;
  padding: 0;
  margin-right: 1.25rem;
  font-size: 1.3rem;
  color: "#888888";
  width: calc(100% - 1.25rem);
  ${StyledOption} {
    border-radius: 0;
    background-color: transparent;
    padding: 0;
    margin: 0;
    color: inherit;
    &:hover {
      border-radius: inherit;
      background-color: inherit;
      padding: inherit;
      margin: inherit;
      color: inherit;
    }
  }
  ${({ isPlaceholder }) =>
    isPlaceholder &&
    css`
      color: #bcbabb;
    `}
`;

Nakonec musíme vyexportovat naši komponentu 👏🏻


import Select from "./select";
import SelectOption from "./select-option";

// Remember this is just a personal preference. It's not mandatory
Select.Option = SelectOption;

export default Select;

Gratulujeme! 🎊, nyní máte vytvořenou opakovaně použitelnou vysoce optimalizovanou komponentu, můžete tento vzor použít v mnoha případech.

Konečný výsledek

Zde se můžete podívat na konečný výsledek: