A repository of bitesize articles, tips & tricks
(in both English and French) curated by Mirego’s team.

Underestimated React.Context feature

Une feature sous-estimée du nouveau Context API de React est qu’un Consumer va toujours consommer la valeur du Provider parent le plus proche. La valeur du context est donc relative à où se situe le component dans le tree, ce qui permet d’avoir multiples valeurs différentes pour un même contexte.

Une porte qu’ouvre cette feature est qu’un Provider de contexte peut lui aussi être un Consumer du même contexte. Ça lui permet de déterminer la valeur du nouveau contexte qu'il crée en se basant sur la valeur actuel du contexte parent.

J’ai justement un excellent use case pour démontrer l’awesomeness de cette feature: un component <Heading/> qui va renderer un tag <h[level]> déterminé automatiquement par son niveau de nesting!

import React, {
  createContext,
  createElement,
  FunctionComponent,
  HTMLAttributes,
  useContext
} from 'react';

type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;

interface HeadingLevelProps {
  override?: HeadingLevel;
}

type HeadingProps = HTMLAttributes<HTMLHeadingElement>;

const HeadingContext = createContext<HeadingLevel>(1);

const isHeadingLevel = (level: number): level is HeadingLevel => {
  return Number.isInteger(level) && level > 0 && level < 7;
};

export const HeadingLevel: FunctionComponent<HeadingLevelProps> = ({
  override,
  children
}) => {
  const level = useContext(HeadingContext);
  const nextLevel = override || level + 1;

  if (!isHeadingLevel(nextLevel)) {
    throw Error(
      `Too much nested headings: h${nextLevel} is not a valid heading element`
    );
  }

  return (
    <HeadingContext.Provider value={nextLevel}>
      {children}
    </HeadingContext.Provider>
  );
};

export const Heading: FunctionComponent<HeadingProps> = ({
  children,
  ...props
}) => {
  const level = useContext(HeadingContext);

  return createElement(`h${level}`, props, children);
};

export default Heading;

Voici comment il s’utilise:

const SomeComponent = () => {
  return (
    <Heading>
      I don’t know which level this will be, but it will be semantically ok!
    </Heading>
  );
};

const App = () => {
  return (
    <div>
      <Heading>This is an h1</Heading>

      <HeadingLevel>
        <Heading>This is an h2</Heading>
        <Heading>This is an h2</Heading>
        <Heading>This is an h2</Heading>

        <HeadingLevel>
          <SomeComponent />{' '}
          {/* The Heading rendered by SomeComponent will be h3! */}
        </HeadingLevel>
      </HeadingLevel>
    </div>
  );
};

⚛️🤘