Setting the Right Context for Your Component Previews

ni
nitsan7702 years ago

Introduction

As a developer working with Bit components, you understand the importance of generating previews ('compositions') to showcase and examine your components in different contexts and variations.

Your task is to create compositions that form the basis for generating these crucial previews. A well-designed composition significantly assists in integrating the component into any work.

There may be instances where a component requires additional context, such as a theme provider or an auth provider. While it's possible to individually wrap each 'composition' with these providers, this isn't considered good practice.

Components use an Env, which dictates how to test, lint, build, and generate their previews it. By configuring your Env with a composition provider, you can automatically wrap all of your components' previews ('compositions').

This post goes through the steps to creating a "composition playground" that allows you to set the context for your compositions, and use an API and UI to control it (for example, to switch from 'light mode' to 'dark mode').

Thanks to the simplified API in the latest Env updates, passing providers to your compositions has become more straightforward. In this tutorial, we will guide you through the process of creating a Composition Playground with an integrated toolbar, ensuring any developer can quickly understand how to use your components effectively.

What are we Going to Build

In this tutorial, we'll create a special component called composition-playground to enhance the display and functionality of your components. Here's an example of a component utilizing the Composition Playground:


You can check the code tab on the right to see how we render this composition. The example displays a simple composition:

export const HomePageWithPlayground = () => {
  return <Home />;
};
CopiedCopy

The toolbar at the top of the composition is not part of the component itself. Instead, it is a composition provider that wraps the component. We will create this special composition provider in this tutorial.

Before diving into the technical details, let's take a look at the dependencies graph:

At the top, you can see the demo pages/home component we just saw. This component uses the envs/composition-providers as its Env.

Next, you can see that the Env relies on the theme-controller, auth-controller, and playground toolbar as its dependencies.

Finally, at the bottom of the hierarchy, there is the composition-playground that serves as the context for all compositions.

How to build it yourself

To start, create a Bit Workspace. I've designed a workspace starter containing all the components from the demo scope. When you run the starter, Bit will fork (copy) the components to your scope, so make sure to replace the default-scope property with your details:

$bit
Copiedcopy

Congratulations! These components are now YOURS.

Let's go over them so you can modify them with your own logic.

The composition-playground component is where you pass the types for the various contexts you want to control:

import { createContext } from 'react';
import { AuthProviderContextType } from '@learnbit-react/state-management.context.auth-provider';
import { ThemeOption } from '@teambit/design.themes.theme-toggler';

export type PlaygroundProps = {
  showToolbar: boolean;

  theme: ThemeOption['themeName'];
  user: AuthProviderContextType;
};

export type CompositionPlaygroundContextType = {
  playground: PlaygroundProps;
  setPlayground: React.Dispatch<React.SetStateAction<PlaygroundProps>>;
};

export const CompositionPlaygroundContext =
  createContext<CompositionPlaygroundContextType>({
    playground: {
      showToolbar: true,
      theme: 'light',
      user: {
        loggedIn: false,
        userName: '',
        avatar: '',
        isAdmin: false,
      },
    },
    setPlayground: () => {},
  });
CopiedCopy

It also exports a usePlayground hook that lets you access the context, as we'll see soon:

import { useContext } from 'react';
import { CompositionPlaygroundContext } from './composition-playground-context';

export const usePlayground = () => useContext(CompositionPlaygroundContext);
CopiedCopy

Next, we have the playground-toolbar component, which is essentially a wrapper to style (or hide) the toolbar controllers. Feel free to modify its styles to suit your needs; we just wanted it to resemble the toolbar above it.

Following that, we have the controllers. They use the usePlayground hook to modify the context state according to the user's selection.

For example, here's the ThemeController:

export function ThemeController() {
  const { setPlayground } = usePlayground();
  return (
    <LightThemeIcon
      style={{ cursor: 'pointer', padding: '0 0.5em' }}
      onClick={() =>
        setPlayground((prev) => ({
          ...prev,
          theme: prev.theme === 'light' ? 'dark' : 'light',
        }))
      }
    >
      Toggle Theme
    </LightThemeIcon>
  );
}
CopiedCopy

If you want to create new controllers, the easiest way would be to use the rename command:

$bit
Copiedcopy

Finally, you have the Env, which mounts all of these providers to each preview generated for the components using it.

There are two important things to notice here:

In the mounter.tsx file, the actual mounting takes place. It's quite straightforward.

import React from 'react';
import { createMounter } from '@teambit/react.mounter';
import { CompositionPlaygroundProvider } from '@learnbit-react/composition-providers.composition-playground';
import { Providers } from './providers';

/**
 * use the mounter to inject and wrap your component previews
 * with common needs like [routing](), [theming]() and [data fetching]().
 */
// eslint-disable-next-line react/prop-types
export function MyReactProvider({ children }) {
  return (
    <CompositionPlaygroundProvider>
      <Providers>{children}</Providers>
    </CompositionPlaygroundProvider>
  );
}

/**
 * to replace that mounter component for different purposes, just return a function
 * that uses ReactDOM to render a node to a div.
 */
export default createMounter(MyReactProvider) as any;
CopiedCopy

In the providers.tsx file, we pass the composition-providers state to the actual providers (use your real providers here!):

export const Providers = ({ children }) => {
  const { playground } = usePlayground();

  return (
    <>
      <PlaygroundToolbar>
        <ThemeController />
        <AuthController />
      </PlaygroundToolbar>
      <Theme activeTheme={playground.theme}>
        <AuthProvider
          loggedIn={playground?.user?.loggedIn}
          userName={playground?.user?.userName}
          avatar={playground?.user?.avatar}
          isAdmin={playground?.user?.isAdmin}
        >
          {children}
        </AuthProvider>
      </Theme>
    </>
  );
};
CopiedCopy

Lastly, pass the package of all the providers, including the composition-playground, to the hostDependencies prop in your my-react-env.bit-env.ts file:

import { ReactEnv } from '@teambit/react.react-env';
import type { ReactEnvInterface } from '@teambit/react.react-env';
import { ReactPreview } from '@teambit/preview.react-preview';
import { EnvHandler } from '@teambit/envs';
import { Preview } from '@teambit/preview';

export class MyReactEnv extends ReactEnv implements ReactEnvInterface {
  name = 'composition-providers';
  icon = 'https://static.bit.dev/extensions-icons/react.svg';

  /**
   * return an instance of a Bit preview instance.
   * notice how we pass all the hostDependencies
   * this way, the preview will be able to resolve them and create only one instance of each.
   */
  preview(): EnvHandler<Preview> {
    return ReactPreview.from({
      mounter: require.resolve('./preview/mounter'),
      hostDependencies: [
        '@learnbit-react/state-management.context.auth-provider',
        '@learnbit-react/composition-providers.toolbar-controllers.theme-controller',
        '@learnbit-react/composition-providers.playground-toolbar',
        '@learnbit-react/composition-providers.toolbar-controllers.auth-controller',
        '@learnbit-react/composition-providers.composition-playground',
        '@mdx-js/react',
        'react',
        'react-dom',
      ],
    });
  }
}

export default new MyReactEnv();
CopiedCopy

This ensures that all the previews use a single instance of the providers. Your demo components should already be set with your env. You can rename it or set other components with this env using the following command:

$bit
Copiedcopy

Don't forget to tag your components:

$bit
Copiedcopy

And export them:

$bit
Copiedcopy

Share Your Composition Providers

We are confident that you can create composition providers that are even better than ours.

Therefore, we encourage you to share your work with the community without hesitation.

Every component you add to the open-source treasury is a valuable asset that can be utilized by others who want to fork or install it as a dependency. We challenge you to create a generic composition playground and share it on our community Slack channel, so that others can benefit from your expertise and contributions.