Hone logo
Hone
Problems

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:

  1. Value Prop: The component must accept a value prop, which represents the current value of the input.
  2. onChange Prop: The component must accept an onChange prop, which is a function that will be called whenever the input's value changes. This function should receive the new value as an argument.
  3. Type Prop: The component should accept a type prop (e.g., "text", "number", "password", "email") to define the input's type.
  4. Placeholder Prop: The component should accept a placeholder prop for accessibility and user guidance.
  5. Label Prop: The component should accept a label prop to associate a <label> element with the input. The label text should be provided.
  6. Accessibility: The <label> element should be correctly associated with the input using htmlFor and id.
  7. TypeScript: All props and internal types should be defined using TypeScript for strong typing.
  8. 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 onChange prop function should be invoked with the updated value.
  • The displayed value in the input field should always reflect the value prop passed to the component.
  • The label should be visible and correctly linked to the input.

Edge Cases:

  • Consider how the component behaves when the value prop is initially empty or null/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 onChange handler should receive the string value from the input event, even if the type is "number". The parent component is responsible for any necessary type conversion.
  • A unique id should 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 onChange callback 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 ...rest operator in your component's props destructuring to capture and spread them onto the native <input> element.
Loading editor...
typescript