Forms should feature intuitive and reusable elements as a part of a modern web development standard. Thus, React in collaboration with TypeScript will allow developers to create input components that will be efficient and scalable as well as type-safe.
Table of Contents
Why Custom Input Components in React with TypeScript?
When developing custom input components in React with TypeScript, it is essential to define clear interfaces for props and state to ensure type safety and improve code readability. TypeScript’s static type checking can help catch errors early in the development process and provide better IDE support for auto-completion and refactoring.
Furthermore, leveraging React’s controlled components pattern and using TypeScript generics can enhance the flexibility and reusability of your custom input components. By following best practices such as proper event handling, validation, and accessibility considerations, you can create robust and accessible input components that contribute to a more maintainable and user-friendly React application.
Custom input components allow for:
- Styling: Consistent styling gives the application a uniform look and feel throughout.
- Handling complex states: Seamless integrations can be done with state management solutions.
- Reusable: Write once, use it anywhere. Do not repeat the same to save time.
- Functionality: Easy integration with extra functionality such as mask, validation, and auto formatting.
Example 1: Custom Input components in React with typescript
In our example we have three function component, first is TextField component, Form wrapper component and App component where we handle the submit function of the Form wrapper component and calling clear function of Form wrapper in its parent component using useImperativeHandle.
In React development where forwardRef
is used to pass down a ref
from a parent component to a child component, allowing direct access to the DOM node or React element. This is especially useful for managing focus, animations, or integrating with third-party DOM libraries. So we are pass ref from App.tsx where we used Form wrapper component and inside we used TextFied component.
However, by default, you cannot pass refs through components because refs are not part of the component’s props. We can’t used ref even we used with ComponentPropsWithoutRef it will not work well. So best solution is to used forwardRef functiont which is provided by React. Forward we not only receive props but also recieve ref parameter.
In our case we passing ref from Parent component App to TextField which is wrapper around the Form component.
In the TextField component we want to used forwardRef to set ref from the outside of this component, where we used it. The TextField
component is defined as a function component using forwardRef
, which allows the component to receive a ref
as the second argument and pass it down to the input
element.
import React, { forwardRef } from "react";
type TextFieldProps = {
label?: string;
} & React.ComponentPropsWithoutRef<"input">;
const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
({ title, ...props }, ref) => {
return (
<label>
{label && <span>{label}</span>}
<input {...props} ref={ref} />
</label>
);
}
);
export default TextField;
The input
is wrapped with the label
tag, which removes the need for the htmlFor
and id
attributes and enhances accessibility. In this function component we used forwardRef. ComponentPropsWithoutRef<"input">
is used to get the props of an input
element excluding ref
Used of forwardRef ?
In React, refs provide a way to access DOM nodes or React elements directly. However, by default, you cannot pass refs through components because refs are not part of the component’s props. This limitation can be a challenge when you need to encapsulate some behaviors in a child component but still want to expose the underlying DOM node or React component instance to parent components.
TheforwardRef
is a React API that allows you to forward a ref
through a component to one of its child components. The forwardRef function is used when you need to access the ref of a child component from a parent component.
How forwardRef
Works
forwardRef
takes a render function as an argument. This render function receives props
and ref
as its parameters and returns a React element. Here’s a simplified structure of how forwardRef
is used:
const ForwardedComponent = React.forwardRef((props, ref) => {
// Return your component logic here, forwarding the `ref` to a DOM element or child component.
});
The forwardRef
isn’t for everyday use. Think of it as a special tool for those specific situations where you need to reach into the inner workings of a component.
Use Cases for forwardRef
1. Accessing DOM Elements:
- Focus input elements directly.
- Measure component size for layout adjustments.
- Create fine-grained DOM animations.
2. Interacting with Child Components:
useImperativeHandle
, we might need to expose specific methods to parent components.forwardRef
enables this interaction.- Expose special methods from child components.
3. Enhancing HOCs and Component Libraries:
- Ensure smooth ref passing for inner components.
- Maintain flexibility for users requiring direct DOM access or method exposure.
Second component we had FormWrapper component to handle
Form Wrapper component
Form component in React that can be used in different forms and has a clear function that can be called from the parent component in our example that is App.tsx.
The Form component that serves as a wrapper around the TextField component. It contains logic for handling form submission and has a clear function that can be called from the parent component. The useImperativeHandle
hook is used to expose the clear
function to the parent component. This function is called when the ref is attached to the component. The clear
function resets the form to its initial state. This is achieved using the useImperativeHandle
hook and the forwardRef
function from React.
import React, { useRef, useImperativeHandle, forwardRef } from "react";
export type FormHandle = { // type for method used in useImperativeHandle hook
clear: () => void;
};
type FormProps = React.ComponentPropsWithoutRef<"form"> & {
onSave: (value: unknown) => void;
};
const Form = forwardRef<FormHandle, FormProps>(function Form(
{ onSave, children, ...otherProps },
ref
) {
const form = useRef<HTMLFormElement>(null);
useImperativeHandle(ref, () => ({
clear() {
form.current?.reset(); //Expose to parent App.tsx
},
}));
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const data = Object.fromEntries(formData);
onSave(data);
}
return (
<form onSubmit={handleSubmit} {...otherProps} ref={form}>
{children}
</form>
);
});
export default Form;
The form ref is different from ref which we passed as first argment in useImperativeHandle hook. The form ref is connected to native form. Now we want to call custom clear method outside of the form wrapper that is inside of App component.
The ref
parameter inside the useImperativeHandle
hook is used to access the underlying instance of a component. It allows you to interact with the component’s methods or properties from outside the component itself.
Use of useImperativeHandle hooks ?
The useImperativeHandle
hook in React allows you to customize the instance value that is exposed to parent components when using a ref. It is used in conjunction with the forwardRef
function to provide a way for child components to expose certain functions or properties to their parent components. The useImperativeHandle hooks only works on component that recieved forwardRef, this hook first argument is ref and second argument is function that return object that can contain any methods that we want to expose to call on outside of this function component.
It’s important to note that the useImperativeHandle
hook should be used sparingly and only when necessary, as it goes against the declarative nature of React. It is typically used in specific cases where imperative code and direct access to component instances are required.
App to consume form wrapper component, Now we need to create ref in App component that can connect to our custom form wrapper component to call custom clear method. Lets create ref inside the App.tsx App
component uses the Form
and TextField components. It creates a ref
called customForm
, which is used to access the Form
instance and call the clear
method in useImperative hook.
import React, { useRef } from "react";
import Input from "./components/Input";
import Form, { FormHandle } from "./components/Form";
import Button from "./components/Button"; // Assuming you have this component
function App() {
const customForm = useRef<FormHandle>(null); //Step 1: to called clear method of Form component
function handleSave(data: unknown) {
// The type assertion below assumes that `data` will have 'name' and 'age' properties.
const extractedData = data as { name: string; age: string };
console.log(extractedData);
customForm.current?.clear(); Step 3: //To invoke clear method inside other component
}
return (
<main>
<Form onSave={handleSave} ref={customForm}>
<Input type="text" label="Name" id="name" />
<Input type="number" label="Age" id="age" />
<p>
<Button>Save</Button>
</p>
</Form>
</main>
);
}
export default App;
FormHandle : is type for import form component, is
const customForm = useRef<FormHandle>(null); . The customForm when connected to form refere not to the Form wrapper component, it refers to the object that return in form wrapper component useImperativeHandle hooks.
Note: It’s important to note that using forwardRef makes the component slightly slower and a bit more memory-intensive, so it is not recommended to use it by default for all components. Only use forwardRef when you need to access the DOM node or pass a ref to a child component.