mediumReact Native#128

Grocery List (CRUD with persistence)

Prompt

Build a grocery list app. Requirements:

  1. TextInput to add items + "Add" button
  2. Show list of items with toggle (strikethrough) and delete
  3. Validate: prevent empty items
  4. Simulate 1-second load delay on startup
  5. Auto-focus the input once loading finishes

Solution

export default function GroceryList() {
  const [items, setItems] = useState([])
  const [input, setInput] = useState('')
  const [loading, setLoading] = useState(true)
  const inputRef = useRef(null)
  useEffect(() => {
    const t = setTimeout(() => setLoading(false), 1000)
    return () => clearTimeout(t)
  }, [])
  useEffect(() => {
    if (!loading) inputRef.current?.focus()
  }, [loading])
  const addItem = () => {
    if (!input.trim()) return
    setItems(prev => [...prev, { id: Date.now(), text: input.trim(), done: false }])
    setInput('')
  }
  const toggleItem = (id) => setItems(prev => prev.map(i => i.id === id ? { ...i, done: !i.done } : i))
  const deleteItem = (id) => setItems(prev => prev.filter(i => i.id !== id))
  if (loading) return <Text>Loading...</Text>
  return (
    <View style={{ padding: 16 }}>
      <View style={{ flexDirection: 'row', gap: 8, marginBottom: 16 }}>
        <TextInput ref={inputRef} value={input} onChangeText={setInput} placeholder="Add item..." style={{ flex: 1, borderWidth: 1, padding: 8, borderRadius: 4 }} />
        <Button title="Add" onPress={addItem} />
      </View>
      <FlatList data={items} keyExtractor={i => String(i.id)} renderItem={({ item }) => (
        <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', padding: 12, borderBottomWidth: 1 }}>
          <Text onPress={() => toggleItem(item.id)} style={{ textDecorationLine: item.done ? 'line-through' : 'none' }}>{item.text}</Text>
          <Button title="X" onPress={() => deleteItem(item.id)} />
        </View>
      )} />
    </View>
  )
}
Mentor's take

CRUD-on-a-list is the smallest app that forces all three immutable update shapes, and interviewers watch for exactly those: add is spread-concat ([...prev, item]), toggle is map-with-spread (copy the one changed object, keep every other reference), delete is filter. All three go through functional updates (setItems(prev => ...)), so rapid taps can't clobber each other by closing over stale state.

Two boundary details carry the seniority signal:

  • Validation at the mutation point: if (!input.trim()) return inside addItem means no caller can insert whitespace items — the invariant lives with the setter, not scattered across the UI.
  • Date.now() as the id, consumed by keyExtractor. With delete in the feature set, index-as-key is actively broken: remove row 2 and every row below shifts its key, so React re-associates row state (the strikethrough, in-flight animations) with the wrong grocery item.

The focus requirement is the imperative escape hatch done correctly: the ref can't be focused on mount because during loading the TextInput isn't rendered at all — so a second effect keyed on loading focuses it once the input actually exists.

Red flag: items.push(...) followed by setItems(items). Same array reference — React bails out of the re-render and the UI silently stops updating. Mutation bugs in RN look like "the list doesn't refresh," not like errors.

Say it: "Add, toggle, delete are spread, map, filter through functional setState — new references on every change, stable ids so delete doesn't shuffle row identity."