3 min read

Switch Your Case with React Components and TypeScript

I have been writing a lot of react lately, and a common feature I reach for is simple matching. weather it be pattern or value, I regularly need to match some value before rendering. conditional rendering is a common task typically accomplished using patterns like

{ some_var && <div>Some component</div> }

However, what if there was a more structured and declarative way to handle these conditions? This curiosity led us to explore a simplified yet powerful approach to conditional rendering using custom Switch and Case components in React with TypeScript.

Introduction

React's built-in conditional rendering methods are effective but can become cumbersome and difficult to manage when dealing with multiple conditions. Inspired by the curiosity to simplify and organize this process, we can create Switch and Case components. These components mimic the behavior of a switch statement in JavaScript, providing a more readable and maintainable solution for complex rendering logic.

Step-by-Step Implementation

We'll start by defining the necessary TypeScript interfaces and then move on to creating the Switch and Case components.

1. Setting Up TypeScript Interfaces

First, we define the TypeScript interfaces to ensure type safety for our components:

import React from "react";

type FunctionalCase = (open: boolean) => any;

export interface ICase<T = any> {
    value: T;
    children: React.ReactNode | FunctionalCase;
}

export interface ISwitch<T = any> {
    value: T;
    unMountTimeout?: number;
    match?: (sval: T, cval: T) => boolean;
    children: React.ReactNode;
}
  • IDefaultCase: An interface for a default case that can render any React node as children.
  • ICase: An interface for each case, which includes a value to match against and children that can be React nodes or a function.
  • ISwitch: An interface for the switch component, including the value to match against, an optional unmount timeout, an optional match function, and children.

2. Creating the Switch Component

The Switch component uses React's Context API to pass down the value and matching logic to its children

function switcher<T = any>(a: T, b: T) {
    return a === b;
}

const SwitchContext = React.createContext<{
    value: any;
    timeout: number;
    match: (a: any, b: any) => boolean;
}>({
    value: undefined,
    timeout: 0,
    match: switcher,
});

export function Switch<T = any>({
    value,
    children,
    unMountTimeout = 0,
    match = switcher,
}: ISwitch<T>) {
    return (
        <SwitchContext.Provider
            value={{ value, match, timeout: unMountTimeout }}>
            <>{children}</>
        </SwitchContext.Provider>
    );
}
  • switcher: A default matching function that checks for strict equality.
  • SwitchContext: A context to provide the current value, match function, and timeout to Case components.
  • Switch: The main component that wraps its children with SwitchContext.Provider, passing down the necessary values.

3. Creating the Case Component

The Case component consumes the context provided by the Switch component to determine whether it should be rendered:

export function Case<T = any>({ value, children }: ICase<T>) {
    const flow = React.useContext(SwitchContext);

    const active = flow.match(flow.value, value);

    const [mount, setMount] = React.useState(active);

    const timeoutRef = React.useRef<ReturnType<typeof setTimeout>>();

    const state = React.useRef<{ active: boolean; timeout: number }>();

    function cleanup() {
        if (timeoutRef.current) {
            clearTimeout(timeoutRef.current);
        }
    }

    React.useEffect(() => cleanup, []);

    React.useEffect(() => {
        if (active) {
            setMount(true);
        } else {
            if (mount === true) {
                if (flow.timeout > 0) {
                    timeoutRef.current = setTimeout(() => {
                        setMount(() => state.current!.active);
                    }, flow.timeout);
                } else {
                    setMount(false);
                }
            }
        }
    }, [active, mount, flow.timeout]);

    state.current = { active, timeout: flow.timeout };

    const trickOrTreat = flow.timeout === 0 ? active : mount;

   

    if (trickOrTreat) {
         return typeof children === "function" ? <>{children(active)}</> : <>{children}</>;
    } else {
        return <></>;
    }
}
  • Case: The component that decides whether to render its children based on the current value from the context and its own value.
  • cleanup: A function to clear any timeouts if the component unmounts.
  • useEffect: Handles the mounting and unmounting logic based on the active state and the timeout.

4. Finally Let Bring the Switch and Case Components all Together

Here's an example of how to use the Switch and Case components in your application:

import React from "react";
import { Switch, Case } from "./SwitchCaseComponents";

function App() {
    const [currentValue, setCurrentValue] = React.useState<number>(1);

    return (
        <div>
            <button onClick={() => setCurrentValue(1)}>Show One</button>
            <button onClick={() => setCurrentValue(2)}>Show Two</button>
            <button onClick={() => setCurrentValue(3)}>Show Three</button>

            <Switch value={currentValue} unMountTimeout={300}>
                <Case value={1}>
                    <div>Case 1: The value is one!</div>
                </Case>
                <Case value={2}>
                    <div>Case 2: The value is two!</div>
                </Case>
                <Case value={3}>
                    <div>Case 3: The value is three!</div>
                </Case>
            </Switch>
        </div>
    );
}

export default App;

By implementing Switch and Case components, you can create a more structured and declarative approach to conditional rendering.

Wait Isn't this more code? Yes but the flow control is descriptive and cool right?

Happy coding!