Skip to content

Instantly share code, notes, and snippets.

@kasper573
Last active November 23, 2020 23:16
Show Gist options
  • Save kasper573/59c7255af1963d605216f905b664c5f2 to your computer and use it in GitHub Desktop.
Save kasper573/59c7255af1963d605216f905b664c5f2 to your computer and use it in GitHub Desktop.
React Template Component

Motivation

In React it's common to need to use a component but alter its layout or behavior to some degree.

This can be done in many ways. Each way has its own benefits and limitations, but I won't go into detail here.

  • Props
<ListItem text="foo" textSize="md"/>
  • Composition
<ListItem>
  <ListItemText size="md">foo</ListItemText>
</ListItem>

This gist demonstrates another approach of solving this problem: createTemplateComponent. This function creates a new component given two parameters:

  • renderElements Returns a record of elements that will be passed as props to the template component.
  • defaultTemplate A react component that renders the elements provided by renderElements. The component returned by createTemplateComponent will use this component by default. This can be overridden by passing in a new template component as children.

The benefit of this approach is that it adds a layer of abstraction where the programmer can define important elements that should be able to be hand picked and reused from inside the component. A great use case is forms, where a templated form component could define all its controls and fields as elements, provide a default generic template while at the same time allow easy customization of the form layout via the template override.

See examples below.

import React, { ComponentType } from "react";
export const createTemplateComponent = <P, E extends Record<any, JSX.Element>>(
renderElements: (props: P) => E,
defaultTemplate: ComponentType<E>
) => ({
children: Template = defaultTemplate,
...props
}: P & { children?: ComponentType<E> }) => (
<Template {...renderElements(props as P)} />
);
import React from "react";
import { createTemplateComponent } from "./createTemplateComponent";
/**
* Form elements displayed as a list by default but with customizable template.
*/
export const Form = createTemplateComponent(
renderElements,
({foo, bar, baz, other}) => (
<ul>
<li>{foo}</li>
<li>{bar}</li>
<li>{baz}</li>
<li>{other}</li>
</ul>
)
);
export type FormValues = {
foo: string;
bar: string;
baz: string;
other: string;
};
function renderElements({
value,
onChange,
}: {
value: TemplateValues;
onChange: (value: TemplateValues) => void;
}) {
const handler = (propName: keyof TemplateValues) => (
e: React.ChangeEvent<HTMLInputElement>
) => onChange({ ...value, [propName]: e.currentTarget.value });
return {
foo: <input value={value.foo} onChange={handler("foo")} />,
bar: <input value={value.bar} onChange={handler("bar")} />,
baz: <input value={value.baz} onChange={handler("baz")} />,
other: <input value={value.other} onChange={handler("other")} />,
};
}
import React, { useState } from "react";
import { Form, FormValues } from "./Form";
function UsageExample () {
const [values, setValues] = useState<FormValues>({
foo: "foo",
bar: "bar",
baz: "baz",
other: "other"
})
return (
<>
<h1>Form with default template</h1>
<Form value={values} onChange={setValues}/>
<h1>Reordering elements and changing layout</h1>
<Form value={values} onChange={setValues}>
{({foo, bar, baz, other}) => (
<>
{other}
<div>{baz}{foo}</div>
{bar}
</>
)}
</Form>
<h1>Rendering only a specific element</h1>
<Form value={values} onChange={setValues}>
{({baz}) => baz}
</Form>
</>
);
}
@mchccn
Copy link

mchccn commented Nov 23, 2020

i get the general idea, this would probably shorten my spaghetti code owo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment