npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

react-compound-components

v0.1.0

Published

Create react compound components

Downloads

4

Readme

React Compound Components

Create compound components with common managed state using React hooks.

Compound components is a pattern where components are used together such that they share an implicit state that lets them communicate with each other in the background. A compound component is composed of a subset of child components that all work in tandem to produce some functionality. - Alexi Taylor - dev.to

Installation

NPM

npm: npm i react-compound-components

Yarn: yarn add react-compound-components

Usage

Import the default exported function from library

import createCompoundComponent from "react-compound-components";
// OR (recommended)
import ccc from "react-compound-components";

ccc

ccc function takes one react-hook function as parameter which is responsible for managing state of the compound component. It returns a tuple of 3 values:

  • Compound component,
  • hook-function to access state, and
  • a register function to register sub-components.
// Naming could/should be changed to suit the case.
const [Component, useCompoundState, register] = ccc(useManageStateHook);

useManageStateHook

In above example, useManageStateHook is a simple react-hook which can take some props and return state value and modifiers as an object.

const useManageStateHook = ({ initialValue }: { initialValue: number }) => {
  const [value, setValue] = React.useState(initialValue);
  const changeValue = (newValue: number) => setValue(value);
  return { value, changeValue };
};

In addition to returning state values and modifiers, an optional Wrapper component can be returned as well. As name suggests, this Wrapper component wraps around the Compound Component. The Wrapper component can only receive children as prop and should return it somehow.

const useManageStateHook = ({ initialValue }: { initialValue: number }) => {
  const [value, setValue] = React.useState(initialValue);
  const changeValue = (newValue: number) => setValue(value);
  const Wrapper: FC = ({ children }) => (
    <div>
      <h1>Compound component</h1>{" "}
      <button onClick={() => changeValue(initialValue)}>Reset</button>
      <hr />
      {children}
    </div>
  );
  return { Wrapper, value, changeValue };
};

Component

The first item in returned tuple is the actual Compound Component which is used to everywhere. This Component acts as a wrapper for all its sub-components. The sub-components can be accessed by using dot-notation of this Component. Eg.

<Component>
  <Component.SubComponent1 />
  <Component.SubComponent2 />
  <Component.SubComponent3 />
</Component>

useCompoundState

The resulting state can be accessed in any sub-component using the useCompoundState hook, which is returned in the tuple.

const SubComponent = () => {
  const { value } = useCompoundState();
  return <pre>{JSON.stringify(value, null, 2)}</pre>;
};

register

A callback provided to simplify attaching Sub-components to parent/main component. It takes 2 parameters

  1. Functional component (mandatory, type: React.FC) - React functional component which return an Element. Use useCompoundState to access state properties. The component can take props as well.
  2. Name (optional, type: string) - In case the functional component is anonymous, the name string provided will be used as the dot-notation reference to the component.
// 1. Register component with anonymous function and name as second parameter
register(() => <div />, "SubComponent1");
// 2. Register component as named component
register(function SubComponent2() {
  return <div />;
});
// 3. Register component with declared function
const SubComponent3 = () => <div />;
register(SubComponent3);

Example

Playground on CodeSandBox.

Basic example

import { useState, FC } from "react";
import { render } from "react-dom";
import ccc from "react-compound-components";

interface Props {
  initState?: number;
}

const [Counter, useCountState, register] = ccc((props: Props) => {
  const { initCount = 0 } = props;
  const [count, setCount] = useState(initCount);
  const increment = () => setCount((count) => count + 1);
  const decrement = () => setCount((count) => count - 1);
  return { count, increment, decrement };
});

register(() => {
  const { increment } = useCountState();
  return <button onClick={increment}>Increase</button>;
}, "Increase");

register(function Decrease() {
  const { decrement } = useCountState();
  return <button onClick={decrement}>Decrease</button>;
});

const Count: FC = () => {
  const { count } = useCountState();
  return <span>{count}</span>;
};
register(Count);

const App = () => (
  <Counter initCount={10}>
    <Counter.Decrease />
    <Counter.Count />
    <Counter.Increase />
  </Counter>
);

render(<App />, document.getElementById("root"));

Advanced example

Note: Not needed for JavaScript

The function assumes that the sub-components do not require/expect props. If any sub-component expects props, then some extra work is needed to be done for TypeScript.

In such case, it is recommended to split the component code to a new file.

// Tabs.tsx
import { useState, useCallback, FC } from "react";
import ccc from "react-compound-components";

interface TabsProps {
  defaultActiveTabId?: string;
}

const [Tabs, useTabs, register] = ccc((props: TabsProps) => {
  const { defaultActiveTabId = "" } = props;
  const [activeTabId, setActiveTabId] = useState(defaultActiveTabId);
  const changeActiveTabId = useCallback(
    (tabId: string) => setActiveTabId(tabId),
    []
  );
  // Optional component to wrap Compound component
  const Wrapper: FC = ({ children }) => (
    <div className="tabs">
      <h1>
        Tabs
        <button onClick={() => changeActiveTabId(defaultActiveTabId)}>
          Reset active tab
        </button>
      </h1>
      {children}
    </div>
  );
  return { Wrapper, activeTabId, changeActiveTabId };
});

interface TabProps {
  tabId: string;
  disabled?: boolean;
}

const Tab: FC<ITabProps> = (props) => {
  const { tabId, disabled, children } = props;
  const { changeActiveTabId } = useTabs();
  return (
    <button
      className="tab"
      disabled={disabled}
      onClick={() => changeActiveTabId(tabId)}
    >
      {children}
    </button>
  );
};

interface PanelProps {
  tabId: string;
}

const Panel: FC<PanelProps> = (props) => {
  const { tabId, children } = props;
  const { activeTabId } = useTabs();
  return activeTabId === tabId ? (
    <div className="tabPanel">{children}</div>
  ) : null;
};

register(Tab);
register(Panel);

// The additional part is forcefully setting component types
// to recognize prop-types of sub-components.
export default (Tabs as unknown) as FC<TabsProps> & {
  Tab: typeof Tab;
  Panel: typeof Panel;
};

and import the component for usage:

// index.tsx
import { FC } from "react";
import { render } from "react-dom";
import Tabs from "./Tabs";

const App: FC = () => (
  <Tabs defaultActiveTabId="tab2">
    <Tabs.Tab tabId="tab1">Tab 1</Tabs.Tab>
    <Tabs.Tab tabId="tab2">Tab 2</Tabs.Tab>
    <Tabs.Tab tabId="tab3" disabled>
      Tab 3
    </Tabs.Tab>
    <hr />
    <Tabs.Panel tabId="tab1">Content of Tab 1</Tabs.Panel>
    <Tabs.Panel tabId="tab2">Content of Tab 2</Tabs.Panel>
    <Tabs.Panel tabId="tab3">Content of Tab 3</Tabs.Panel>
  </Tabs>
);

render(<App />, document.getElementById("root"));

Contributing

If you find a bug, please create an issue providing instructions to reproduce it. It's always very appreciable if you find the time to fix it. In this case, please submit a PR.

If you're a beginner, it'll be a pleasure to help you contribute. You can start by reading the beginner's guide to contributing to a GitHub project.

Know issues

  • When registering a sub-component, the TypeScript type of Sub-component isn't inferred automatically.

License

MIT © Siddhant Gupta