Imagine you're working on creating a new React-Bootstrap page and realize you need to use a component that's slightly different from an existing component. There are a few different ways this could be done.
Depending on the context, the existing component could be parameterized to support the new functionality. This works in some scenarios, but it can often lead to components with many mismatched features that are difficult to maintain. Another option is to simply copy and paste the existing component. If the shared functionality is only going to be duplicated in two components, this might make sense. This isn't a great solution if the functionality is going to be duplicated in three or more components though.
This is where dynamically generated components come in. Since React allows you to use simple functions as components and JavaScript supports first-class functions, a function can be created which dynamically generates new components.
In this example, the React-Bootstrap <Toast>
element will be used.
1import React from 'react';
2import Toast from 'react-bootstrap/Toast';
3import Container from 'react-bootstrap/Container';
4
5function ErrorToast() {
6 return (
7 <Toast>
8 <Toast.Header>Error</Toast.Header>
9 <Toast.Body>Something went wrong.</Toast.Body>
10 </Toast>
11 );
12}
13
14export default function App() {
15 return <Container>
16 <ErrorToast />
17 </Container>
18}
This new <ErrorToast>
element will create a new React-Bootstrap
<Toast>
element with a preset header and body. What if you wanted to
create a <WarningToast>
or a <NotifyToast>
? Ideally you wouldn't
need to duplicate the entire component just to modify the preset text.
The first step is to create a function that returns function components.
1import React from 'react';
2import Toast from 'react-bootstrap/Toast';
3
4function createToast() {
5 return () => (
6 <Toast>
7 <Toast.Header>Error</Toast.Header>
8 <Toast.Body>Something went wrong.</Toast.Body>
9 </Toast>
10 );
11}
This function will allow you to create an ErrorToast
component.
1const ErrorToast = createToast();
Obviously this doesn't provide much benefit until the createToast
function is updated to accept some arguments.
1import React from 'react';
2import Toast from 'react-bootstrap/Toast';
3
4function createToast({ header }) {
5 return () => (
6 <Toast>
7 <Toast.Header>{ header }</Toast.Header>
8 <Toast.Body>Something went wrong.</Toast.Body>
9 </Toast>
10 );
11}
This is great if all you wanted to do was create components with
different headers, but what about the <Toast.Body>
component? You
could just pass a value in the same way as header
, but what if you
want the body to be different for each instance of the toast? This
could be done one of two ways. The toast element could accept a
attribute, or it could use the children
attribute to use any
elements or text nested under the element.
1import React from 'react';
2import Toast from 'react-bootstrap/Toast';
3
4function createToast({ header }) {
5 return ({ children }) => (
6 <Toast>
7 <Toast.Header>{ header }</Toast.Header>
8 <Toast.Body>{ children }</Toast.Body>
9 </Toast>
10 );
11}
Now any children of any elements created with this function will be
nested under the <Toast.Body>
element.
One last thing to add to the component is some local state. The
React-Bootstrap <Toast>
component has a show
attribute which
allows it to be hidden as well as an onClose
attribute which gets
called when the close button is clicked.
1import React, { useState } from 'react';
2import Toast from 'react-bootstrap/Toast';
3
4function createToast({ header }) {
5 const [show, changeShow] = useState(true);
6
7 return ({ children }) => (
8 <Toast show={show} onClose={() => changeShow(false)}>
9 <Toast.Header>{ header }</Toast.Header>
10 <Toast.Body>{ children }</Toast.Body>
11 </Toast>
12 );
13}
This will allow the toasts to be dismissed.
Now that you've created the createToast
function, it can be used to
create new components.
1const ErrorToast = createToast({ header: "Error" });
2const WarningToast = createToast({ header: "Warning" });
With these components created, here's how they would be used in the UI.
1import React from 'react';
2import Container from 'react-bootstrap/Container';
3
4export default function App() {
5 return <Container>
6 <ErrorToast>Something went wrong.</ErrorToast>
7 <WarningToast>You shouldn't do that.</WarningToast>
8 </Container>
9}
Using dynamic functions to create components is a useful pattern for avoiding duplication. Whenever you find yourself repeating the same pattern across multiple components or thinking of duplicating an existing component to make small modifications, you should consider creating a function to dynamically generate those components for you.