Implement a useArray hook that manages an array of items with additional utility methods.
It is more convenient to use useArray than plain useState; with useState, you have to derive a new array and set state with that array for every update, which can be cumbersome.
The hook should work generically with arrays of any type.
const defaultValue = ['apple', 'banana'];export default function Component() {const { array, push, update, remove, filter, set, clear } =useArray(defaultValue);return (<div><p>Fruits: {array.join(', ')}</p><button onClick={() => push('orange')}>Add orange</button><button onClick={() => update(1, 'grape')}>Change second item to grape</button><button onClick={() => remove(0)}>Remove first</button><button onClick={() => filter((fruit) => fruit.includes('a'))}>Keep fruits containing 'a'</button><button onClick={() => set(defaultValue)}>Reset</button><button onClick={clear}>Clear list</button></div>);}
defaultValue: The initial array of itemsThe hook returns an object with the following properties:
array: The current array of itemsset: (newArray) => void: A function that sets the array of items. It must have the same type as the setter returned by useStatepush: (item) => void: A function that adds an item to the end of the arrayremove: (index: number) => void: A function that removes an item from the array by indexfilter: (predicate) => void: A function that filters the array based on a predicate function. predicate must have the same type as the callback passed to Array.prototype.filterupdate: (index: number, newItem) => void: A function that replaces an item in the array at indexclear: () => void: A function that clears the arrayarray directly.set should behave like the setter returned by useState, including accepting updater functions.clear() should replace the array with an empty array.The hook owns one React state array and exposes a few immutable transformations. array is the current state value, set is the raw React setter, and the other methods are convenience wrappers around that setter.
Helpers should never mutate the current array in place. React compares state by reference, so each helper creates the next array reference instead:
The helper jobs are:
push appends one element to the previous array.filter delegates to the caller's predicate and keeps the values it accepts.update replaces one index by rebuilding the prefix and suffix around it.remove rebuilds the array without the target index.clear replaces state with a fresh empty array.set stays as the raw React setter for operations outside the convenience API.const push = useCallback((element) => {setArray((array) => [...array, element]);}, []);
The functional setter form is important whenever the next array depends on the previous one. It makes every operation run against React's latest state, even if several updates are queued before the next render.
filter can delegate directly to Array.prototype.filter because it already returns a new array. update and remove rebuild the array from slices around the target index:
const update = useCallback((index, newElement) => {setArray((array) => [...array.slice(0, index),newElement,...array.slice(index + 1),]);}, []);
clear does not need the previous value, so it can simply replace state with a fresh empty array. The custom helpers are wrapped in useCallback because their behavior only depends on setArray, whose identity is stable.
The helpers line up with a small set of immutable transformations:
| Helper | Next array shape |
|---|---|
push(x) | old items followed by x |
filter(fn) | items for which fn returns truthy |
update(i, x) | prefix before i, then x, then suffix after i |
remove(i) | prefix before i, then suffix after i |
clear() | [] |
Because set is the raw React setter, callers can still use set((array) => array.slice().reverse()) for operations that are not covered by the convenience helpers.
That raw setter is the intentional escape hatch. Hiding it would make the hook feel safer, but it would also make normal useState update patterns unavailable to consumers.
import { useCallback, useState } from 'react';/*** @template T* @param {T[]} defaultValue*/export default function useArray(defaultValue) {const [array, setArray] = useState(defaultValue);const push = useCallback((element) => setArray((a) => [...a, element]), []);const filter = useCallback((callback) => setArray((a) => a.filter(callback)),[],);const update = useCallback((index, newElement) =>setArray((a) => [...a.slice(0, index),newElement,...a.slice(index + 1, a.length),]),[],);const remove = useCallback((index) =>setArray((a) => [...a.slice(0, index), ...a.slice(index + 1, a.length)]),[],);const clear = useCallback(() => setArray([]), []);return { array, set: setArray, push, filter, update, remove, clear };}
Mutating the current array: Calling methods like array.push(element) and then setting the same array reference mutates React state directly. It can prevent rerenders and violates the expected immutable behavior.
Reading array from the render closure: Writing setArray([...array, element]) works for a single click, but it can drop updates when multiple operations are queued together. Use setArray((array) => nextArray) for helpers that depend on the previous contents.
Replacing the raw setter with a narrower helper: The returned set should be the same kind of setter returned by useState, so callers can pass either a new array or an updater function. Returning setArray directly preserves that behavior.
defaultValue is only the initial state value. The hook does not sync an already-mounted array when the argument changes.update and remove rely on normal JavaScript array slicing behavior rather than adding custom index validation.console.log() statements will appear here.