Why changing one item changes all of them — three causes and fixes

You're building tic-tac-toe. You place an X in the top-left corner. And the X shows up in the entire left column — in all three rows at once. You placed one. The code shows three.
It's not a rendering glitch. In memory there really is just one row — three different places are looking at it. Change it through one, and everyone sees it.
The symptom is always the same: "I change one item and they all change." For beginners there are usually three causes. Let's start with the most common.
First: how to check this is the problem
One line in the console:
console.log(grid[0] === grid[1])
For objects and arrays, === doesn't compare the contents — it compares the address. true means it's literally the same row under two numbers. Below is where those twins come from. Why a variable holds an address instead of the thing itself is covered in what is a variable.
Cause 1: fill put one object in every slot
The shortest way to make a 3×3 board — and the sneakiest:
const grid = Array(3).fill([])
grid[0].push("X")
// [["X"], ["X"], ["X"]]
fill doesn't create three new arrays. It creates one [] and puts a reference to it in every slot. Three slots — one box.
How to check. grid[0] === grid[1] returns true.
How to fix. Give each slot its own array. For that you need a function that runs again for every element:
const grid = Array.from({ length: 3 }, () => [])
() => [] runs three times — three different arrays.
In Python the same trap looks like [[0] * 3] * 3. Multiplying a list copies references. Change board[0][0] and the first item changes in every row. The fix is a comprehension: [[0] * 3 for _ in range(3)].
fill with numbers and strings is safe: Array(3).fill(0) is fine. The trouble is only with objects and arrays.
Cause 2: one object reused in a loop
You're building a list of rows for a table:
const rows = []
const row = {}
for (const name of ["Anna", "Bob", "Vicky"]) {
row.name = name
rows.push(row)
}
// [{name: "Vicky"}, {name: "Vicky"}, {name: "Vicky"}]
You pushed the same row into the list three times. On each step you overwrote it. At the end it holds the last name — and all three rows see it.
This bug loves AI-written code where the object was "moved up for tidiness".
How to check. All the items are identical and equal to the last value. That's the giveaway.
How to fix. Create a new object inside the loop:
for (const name of names) {
rows.push({ name })
}
Or shorter: const rows = names.map((name) => ({ name })).
Cause 3: a shared "default" object
Python has a famous trap:
def add(item, items=[]):
items.append(item)
return items
add(1) # [1]
add(2) # [1, 2] — expected [2]
The default list is created once, when Python reads the function. After that, every call appends to the same list. The second shopper's cart suddenly holds the first shopper's items.
In JavaScript, function add(item, items = []) doesn't break like this — the default is created fresh on every call. But the same problem sneaks in another way: a shared settings object kept in a constant.
const DEFAULTS = { tags: [] }
const a = { ...DEFAULTS }
a.tags.push("new")
// DEFAULTS.tags is now ["new"] too
The ... spread copied only the top level. The tags array stayed shared by everyone who took the "default settings".
How to fix. In Python — items=None, then inside if items is None: items = []. In JavaScript — make defaults a function that builds a new object every time: const makeDefaults = () => ({ tags: [] }). Or a deep copy: structuredClone(DEFAULTS).
Why it matters at all
A related bug in React looks different: you changed an object in place — and the screen didn't update. React compares addresses. Same address means "nothing changed". More in the article on state.
One rule covers all three cases: if you need a new object, you have to create it explicitly. The {} or [] has to run as many times as the number of separate copies you need.
Ask an AI to check your code with this prompt: "Find places where several array items may reference the same object: fill with an object, an object created outside a loop, a shared default object."
Why doesn't this happen with numbers?
Numbers, strings and true/false are copied whole. Objects and arrays are passed by address. That's why fill(0) is safe and fill([]) isn't.
Is structuredClone always the answer?
No, it's a last resort. A deep copy is slower and doesn't copy functions. Usually it's enough to create objects in the right place instead of copying finished ones.
Short story-lessons, an agent simulator and daily practice — in our mobile app. Free.





