Building a Controlled Input Component in React with TypeScript
This challenge focuses on creating a robust and reusable "controlled input" component in React. Controlled components are fundamental for managing form state effectively, ensuring that React state is the single source of truth for the input's value. This is crucial for building dynamic and interactive user interfaces.
Problem Description
Your task is to create a React functional component called ControlledInput. This component should wrap a standard HTML <input> element and provide a controlled way to manage its value.
Key Requirements:
- Value Prop: The component must accept a
valueprop, which represents the current value of the input. - onChange Prop: The component must accept an
onChangeprop, which is a function that will be called whenever the input's value changes. This function should receive the new value as an argument. - Type Prop: The component should accept a
typeprop (e.g., "text", "number", "password", "email") to define the input's type. - Placeholder Prop: The component should accept a
placeholderprop for accessibility and user guidance. - Label Prop: The component should accept a
labelprop to associate a<label>element with the input. The label text should be provided. - Accessibility: The
<label>element should be correctly associated with the input usinghtmlForandid. - TypeScript: All props and internal types should be defined using TypeScript for strong typing.
- Customizable Props: The component should allow passing down any other standard HTML input attributes (like
className,disabled,aria-label, etc.) to the underlying<input>element.
Expected Behavior:
- When the user types into the input field, the
onChangeprop function should be invoked with the updated value. - The displayed value in the input field should always reflect the
valueprop passed to the component. - The label should be visible and correctly linked to the input.
Edge Cases:
- Consider how the component behaves when the
valueprop is initially empty ornull/undefined. - Ensure that passing arbitrary HTML attributes works as expected.
Examples
Example 1: Basic Text Input
Parent Component (App.tsx):
import React, { useState } from 'react';
import ControlledInput from './ControlledInput'; // Assuming ControlledInput.tsx
function App() {
const [name, setName] = useState('');
return (
<div>
<ControlledInput
label="Full Name"
type="text"
value={name}
onChange={setName}
placeholder="Enter your full name"
/>
<p>Current Name: {name}</p>
</div>
);
}
export default App;
ControlledInput.tsx (Conceptual Output):
<div>
<label htmlFor="controlled-input-name">Full Name</label>
<input
id="controlled-input-name"
type="text"
value="[User's input]"
placeholder="Enter your full name"
onChange={(e) => /* calls setName with e.target.value */}
/>
</div>
Explanation:
The App component manages the name state. It passes name as the value prop and setName as the onChange prop to ControlledInput. When the user types, ControlledInput calls setName, updating the state in App, which then re-renders ControlledInput with the new value.
Example 2: Number Input with Custom Class
Parent Component (App.tsx):
import React, { useState } from 'react';
import ControlledInput from './ControlledInput';
function App() {
const [age, setAge] = useState<number | ''>('');
return (
<div>
<ControlledInput
label="Age"
type="number"
value={age}
onChange={(newAge) => setAge(newAge)}
placeholder="Your age"
className="my-custom-input"
disabled={false} // Example of passing another prop
/>
<p>Current Age: {age}</p>
</div>
);
}
export default App;
ControlledInput.tsx (Conceptual Output):
<div>
<label htmlFor="controlled-input-age">Age</label>
<input
id="controlled-input-age"
type="number"
value={[User's input, potentially converted to string for input']}
placeholder="Your age"
className="my-custom-input"
disabled={false}
onChange={(e) => /* calls setAge with Number(e.target.value) or '' if invalid */}
/>
</div>
Explanation:
Similar to Example 1, but demonstrates using type="number" and passing custom attributes like className and disabled. The onChange handler might need to parse the value to a number.
Example 3: Handling Empty/Initial State
Parent Component (App.tsx):
import React, { useState } from 'react';
import ControlledInput from './ControlledInput';
function App() {
const [password, setPassword] = useState(''); // Initially empty string
return (
<div>
<ControlledInput
label="Password"
type="password"
value={password}
onChange={setPassword}
placeholder="Create a strong password"
/>
</div>
);
}
export default App;
ControlledInput.tsx (Conceptual Output):
<div>
<label htmlFor="controlled-input-password">Password</label>
<input
id="controlled-input-password"
type="password"
value="" // Initially empty
placeholder="Create a strong password"
onChange={(e) => /* calls setPassword with e.target.value */}
/>
</div>
Explanation:
This shows the component correctly rendering an empty input when the value prop is an empty string. The placeholder is visible until the user starts typing.
Constraints
- The component must be a functional component.
- The component must use TypeScript for prop definitions.
- The
onChangehandler should receive the string value from the input event, even if thetypeis "number". The parent component is responsible for any necessary type conversion. - A unique
idshould be generated for each input to ensure proper label association. You can use a simple counter or a library if preferred, but for this challenge, a simple unique ID generation strategy is sufficient. - The component should aim for reusability and avoid hardcoded values beyond basic structure.
Notes
- Consider using a generic type for the
onChangecallback if you intend to support more complex scenarios, but for this challenge, a simple function accepting a string is sufficient. - Think about how to handle potential errors or validation, although implementing full validation is out of scope for this particular challenge.
- The way you generate unique IDs is up to you, but ensure they are unique within the scope of where the component is used if multiple instances are present. For simplicity, you can prepend a common prefix like
controlled-input-to a generated ID. - When passing down other props, use the
...restoperator in your component's props destructuring to capture and spread them onto the native<input>element.