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:
- The current state value.
- 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
Tfor the state. - It should accept an optional
initialStateof typeT. - It should return a tuple
[state, setState]. - The
setStatefunction must handle:- Receiving a partial object of type
Partial<T>and merging it with the current state ifTis an object type. - Receiving a new state value of type
Tand replacing the current state ifTis not an object type. - Receiving a state updater function
(prevState: T) => Tfor both object and non-object state types.
- Receiving a partial object of type
Expected Behavior:
- When the initial state is an object, calling
setState({ property: newValue })should update onlypropertyand 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 withnewValue. - The updater function signature
(prevState) => newStateshould work for both object and primitive states.
Edge Cases to Consider:
- What happens if the initial state is
nullorundefined? - How should the hook behave if
initialStateis 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
useStatefrom React internally. - The state merging logic should only apply if the state type
Tis 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
setStatefunction should correctly handle the updater function signature(prevState: T) => T.
Notes
- Consider how to detect if
Tis a "plain object" for the merging logic. You might need to checktypeof 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 bePartial<T>(ifTis an object) orT(ifTis not an object), or a function.
Good luck! This exercise will help you understand custom hooks and advanced state management patterns in React.