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 toCase
components.Switch
: The main component that wraps its children withSwitchContext.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!
Member discussion