Creating Reusable Components with useMemo
Published: January 04, 2021Today we will looking at a principle of creating resilient components in React and creating a useful custom hook with the useMemo hook. Sometimes when creating a component, we can assume that it will only be used once.
However, that assumption may lead into problems sharing props when reusing a simple component. In our example here today, we will look at creating a resilient form component.
Sometimes we assume a certain component is only ever displayed once. Such as a navigation bar. This might be true for some time. However, this assumption often causes design problems that only surface much later. - Dan Abramov
You can check out the completed code to follow along here and the demo in action here.
The Setup
We will start by bootstrapping a project with create-react-app and installing the react-bootstrap and shortid packages. I will be using the shortid package in this demo to create our randomly generated unique IDs, but you can use any method you like to create a random ID string.
npx create-react-app usememo-resilient-components;
cd usememo-resilient-components;
npm i react-boostrap shortid;
npm start;
Don't forget to add the CDN link for the bootstrap css files as react-bootstrap does not include them. You can follow the react-bootstrap docs here.
// index.html
...
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk"
crossorigin="anonymous"
/>
...
Creating the form handler
Let's start by going into our App.js, clearing out the boilerplate, and creating our form handler. We will set an empty object using the useState hook to store our parameters. To maximize modularity, in our form handler function we will pass a callback to setParams that target the name and value attributes of as many form elements as necessary.
Also, let's import some bootstrap elements from our react-bootstrap package and set the container and column to display for now. Your App.js should look like this:
// App.js
import { useState } from "react";
import { Container, Col } from "react-bootstrap";
function App() {
const [params, setParams] = useState({});
const handleChange = (e) => {
const param = e.target.name;
const value = e.target.value;
setParams((prevParams) => {
return { ...prevParams, [param]: value };
});
};
return (
<Container>
<Col lg="5">
<h1>Generate Unique IDs Demo</h1>
</Col>
</Container>
);
}
export default App;
Creating our form and displaying the input
Next, create we will create our form component. For this demo, I have imported a basic two element form from react-bootstrap, but feel free to add as many form elements as you want. Our component will be passed our handler function as a prop from App.js to handle our form onChange attribute.
// InputForm.js
import { Form } from "react-bootstrap";
export default function InputForm({ handleChange }) {
return (
<Form className="mb-2">
<Form.Group>
<Form.Label>Email</Form.Label>
<Form.Control
type="email"
placeholder="Enter email"
name="email"
onChange={handleChange}
/>
</Form.Group>
<Form.Group>
<Form.Label>Name</Form.Label>
<Form.Control
type="text"
placeholder="Enter Name"
name="name
onChange={handleChange}
/>
</Form.Group>
</Form>
);
}
To see what our returned parameters are in state, we could simply log them to our console, but I have created a component to display our parameters on our page for this demo. My component will map through all of the parameters passed from our form handler in App.js and display them in a Jumbotron element.
// DisplayInput.js
...
import React from "react";
import { Jumbotron } from "react-bootstrap";
export default function DisplayInput({ params }) {
const paramKeys = Object.keys(params);
return (
<Jumbotron>
{paramKeys.map((key) => {
return <p key={key}>{key + ": " + params[key]}</p>;
})}
</Jumbotron>
);
}
...
Now we can import both elements into our App.js and pass the handler to our form component and pass our parameters in state to our display component.
// App.js
...
import DisplayInput from "./Components/DisplayInput";
import InputForm from "./Components/InputForm";
...
return (
<Container>
<Col lg="5">
<h1>Generate Unique IDs Demo</h1>
<InputForm handleChange={handleChange} />
<DisplayInput params={params} />
</Col>
</Container>
);
...
When you run npm start, you should be able to see our the name and value of our inputs displayed in our Jumbotron.
The Problem
This becomes problematic when you want to reuse the same form component again with the same handler. In our App.js, let's render our form component again.
// App.js
...
return (
<Container>
<Col lg="5">
<h1>Generate Unique IDs Demo</h1>
<InputForm handleChange={handleChange} />
<InputForm handleChange={handleChange} />
<DisplayInput params={params} />
</Col>
</Container>
);
...
You can see in the figure below that our parameters prop will not distinguish between our inputs from both form components:
We could solve this issue with writing a separate form handler function. But let's say that you want to use the form component multiple more times. You would have to create a separate function to pass to each form.
Creating a custom useMemo hook
useMemo is a hook included in react that returns a memoized value. This means that it will only fire if one of its dependencies changes. This means that it will not recalculate methods that are computationally expensive upon each re-render, if its passed values do not change. You can read more about it in the react hooks docs here.
Under a new directory, we will create our custom useUniqueId.js hook file. We will create a variable and pass useMemo our id generator function and an empty array. Like with the useEffect hook, the empty array will tell useMemo to only fire once per component render. Lastly, our hook will return generated unique id with our element name suffix.
// useUniqueId.js
...
import { useMemo } from "react";
const shortid = require("shortid");
const useUniqueId = () => {
const uniqueId = useMemo(() => shortid.generate(), []);
return (elementNameSuffix) => `${uniqueId}_${elementNameSuffix}`;
};
export default useUniqueId;
...
Now we can import our custom useUniqueId hook into our form component and pass it to our name attributes in the form elements.
// useUniqueId.js
...
import { Form } from "react-bootstrap";
import useUniqueId from "../helpers/useUniqueId";
export default function InputForm({ handleChange }) {
const generateId = useUniqueId();
return (
<Form className="mb-2">
<Form.Group>
<Form.Label>Email</Form.Label>
<Form.Control
type="email"
placeholder="Enter email"
name={generateId("email")}
onChange={handleChange}
/>
</Form.Group>
<Form.Group>
<Form.Label>Name</Form.Label>
<Form.Control
type="text"
placeholder="Enter Name"
name={generateId("name")}
onChange={handleChange}
/>
</Form.Group>
</Form>
);
}
...
Finally, let's run our app again and see the result.
As you can see, now our parameters can be distinguished uniquely from each other, no matter how many times we render our form component!
Congratulations!!
We now our form components can be rendered multiple times without having to worry about our parameters being lost and overwritten. Our custom hook can also be used on any attribute in any component that you want to re-use.
You can check out the completed code here and the demo in action here.
If you like it, share it!