Hone logo
Hone
Problems

Implement a useSetState Hook in React

React's built-in useState hook is excellent for managing simple state, but it only allows you to update one part of the state at a time. Often, components have state that is an object, and you want to merge updates into that object rather than replacing it entirely. This challenge asks you to implement a custom hook, useSetState, that mimics the behavior of this.setState from class components, allowing for partial updates to an object-based state.

Problem Description

You need to create a custom React hook named useSetState in TypeScript. This hook should accept an initial state value (which can be an object or any other type) and return an array containing two elements:

  1. The current state value.
  2. A function to update the state.

The key difference from useState is how the update function behaves. When called with an object, this update function should merge the provided object into the current state, rather than replacing it entirely. If the initial state is not an object, the update function should behave like the standard useState updater function.

Key Requirements:

  • The hook must be named useSetState.
  • It should accept a generic type T for the state.
  • It should accept an optional initialState of type T.
  • It should return a tuple [state, setState].
  • The setState function must handle:
    • Receiving a partial object of type Partial<T> and merging it with the current state if T is an object type.
    • Receiving a new state value of type T and replacing the current state if T is not an object type.
    • Receiving a state updater function (prevState: T) => T for both object and non-object state types.

Expected Behavior:

  • When the initial state is an object, calling setState({ property: newValue }) should update only property and keep other properties of the state intact.
  • When the initial state is a primitive (like a number, string, boolean), calling setState(newValue) should replace the current state with newValue.
  • The updater function signature (prevState) => newState should work for both object and primitive states.

Edge Cases to Consider:

  • What happens if the initial state is null or undefined?
  • How should the hook behave if initialState is not provided?
  • Ensure type safety when merging objects.

Examples

Example 1: Object State Merging

import React, { useState } from 'react';
import { useSetState } from './useSetState'; // Assuming your hook is in './useSetState'

interface UserProfile {
  name: string;
  age: number;
  occupation?: string;
}

function UserProfileComponent() {
  const [profile, setProfile] = useSetState<UserProfile>({ name: 'Alice', age: 30 });

  const updateAge = () => {
    setProfile({ age: profile.age + 1 });
  };

  const updateOccupation = () => {
    setProfile({ occupation: 'Engineer' });
  };

  return (
    <div>
      <p>Name: {profile.name}</p>
      <p>Age: {profile.age}</p>
      {profile.occupation && <p>Occupation: {profile.occupation}</p>}
      <button onClick={updateAge}>Increase Age</button>
      <button onClick={updateOccupation}>Set Occupation</button>
    </div>
  );
}

Input: User clicks "Increase Age" then "Set Occupation".

Output (after both clicks):

Name: Alice
Age: 31
Occupation: Engineer

Explanation: Initially, profile is { name: 'Alice', age: 30 }. Clicking "Increase Age" calls setProfile({ age: profile.age + 1 }). This merges { age: 31 } into the state, resulting in { name: 'Alice', age: 31 }. Clicking "Set Occupation" calls setProfile({ occupation: 'Engineer' }). This merges { occupation: 'Engineer' } into the state, resulting in { name: 'Alice', age: 31, occupation: 'Engineer' }.

Example 2: Primitive State Update

import React from 'react';
import { useSetState } from './useSetState';

function CounterComponent() {
  const [count, setCount] = useSetState<number>(0);

  const increment = () => {
    setCount(count + 1); // Behaves like useState's direct value update
  };

  const incrementByUpdater = () => {
    setCount((prevCount) => prevCount + 5); // Behaves like useState's updater function
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={incrementByUpdater}>Increment by 5</button>
    </div>
  );
}

Input: User clicks "Increment" then "Increment by 5".

Output (after both clicks):

Count: 6

Explanation: Initially, count is 0. Clicking "Increment" calls setCount(count + 1), which is setCount(1). State becomes 1. Clicking "Increment by 5" calls setCount((prevCount) => prevCount + 5). With prevCount being 1, the new state becomes 6.

Example 3: Initial State with undefined and Nulls

import React from 'react';
import { useSetState } from './useSetState';

interface Settings {
  theme?: 'light' | 'dark';
  notificationsEnabled: boolean | null;
}

function SettingsComponent() {
  const [settings, setSettings] = useSetState<Settings>({ notificationsEnabled: null });

  const enableNotifications = () => {
    setSettings({ notificationsEnabled: true });
  };

  const setThemeToDark = () => {
    setSettings({ theme: 'dark' });
  };

  return (
    <div>
      <p>Theme: {settings.theme || 'Not set'}</p>
      <p>Notifications Enabled: {settings.notificationsEnabled === null ? 'Not configured' : settings.notificationsEnabled.toString()}</p>
      <button onClick={enableNotifications}>Enable Notifications</button>
      <button onClick={setThemeToDark}>Set Theme to Dark</button>
    </div>
  );
}

Input: User clicks "Enable Notifications" then "Set Theme to Dark".

Output (after both clicks):

Theme: dark
Notifications Enabled: true

Explanation: Initially, settings is { notificationsEnabled: null }. theme is undefined. Clicking "Enable Notifications" merges { notificationsEnabled: true }, resulting in { notificationsEnabled: true }. Clicking "Set Theme to Dark" merges { theme: 'dark' }, resulting in { theme: 'dark', notificationsEnabled: true }.

Constraints

  • Your hook must be implemented in TypeScript.
  • You can use useState from React internally.
  • The state merging logic should only apply if the state type T is a plain object (i.e., not an array, null, or other built-in types that have properties but aren't meant for merging in this context).
  • The setState function should correctly handle the updater function signature (prevState: T) => T.

Notes

  • Consider how to detect if T is a "plain object" for the merging logic. You might need to check typeof state === 'object' && state !== null && !Array.isArray(state).
  • When merging objects, ensure that existing properties are overwritten and new properties are added.
  • Think about how to handle the type of the update payload for setState. It can be Partial<T> (if T is an object) or T (if T is not an object), or a function.

Good luck! This exercise will help you understand custom hooks and advanced state management patterns in React.

Loading editor...
typescript