← Concepts & practices
Concept Language and runtime models

Values and references

Follow what a write reaches.

You already pass objects to functions and expect some changes to show up and others not to. Most of the time you guess right. Let’s follow a report’s settings through three lines that look alike and land in different places, and find the one that quietly changes a shared preset.

TypeScriptGo One settings example, two implementations.

01 / The idea

Letting a helper edit the settings is a fair start.

You’re building a report builder. The open report’s settings are held by the settings form and by a live preview, and a helper, applyPageSize, edits them. Both screens reach the same object, so the preview updates without anyone copying anything.

Read the first helperTypeScript · the version this lesson starts from
settings.ts
// The first version: a helper edits the settings it's given. Every screen holding them sees it.
export function applyPageSize(settings: ReportSettings, pageSize: number): void {
	settings.pageSize = pageSize;
}

Go’s version takes a pointer to the settings. Both languages meet again at writeThrough in section 02.

Then a teammate writes a helper that starts from a shared preset. It assigns the preset to its parameter and sets the page size to 99. The caller’s settings don’t change at all. The preset does, and every report that starts from it now shows 99 rows. Nobody meant to write to the preset.

A name holds a value. For an object, that value is a reference: a way to reach the object, not the object itself. Assigning to a name changes what it reaches. Writing a property changes the object, for every name that reaches it. JavaScript and Go both pass arguments by value, and when the value is a reference, the caller and the function reach the same object.

React’s documentation turns that into an everyday rule: “treat any JavaScript object that you put into state as read-only.” Section 05 shows why, with a list of reports that doesn’t update.

02 / See the shape

Three lines that look alike, and one way to stop sharing.

The basic form is three functions: assign a number parameter, write through an object parameter, reassign an object parameter. In the wild adds settings nobody can edit and a function that returns new settings instead of changing old ones. At the call site runs all of them.

Both languages run the same functions and produce the same six numbers.

Three lines that look alike: assigning a number parameter, writing through an object parameter, and reassigning an object parameter. Only the middle one reaches the caller.

TypeScriptReading
settings.ts
// Three lines that look alike and write to different places.
export function changeNumber(pageSize: number, next: number): number {
	pageSize = next; // Changes only this parameter.
	return pageSize;
}

export function writeThrough(settings: ReportSettings, next: number): void {
	settings.pageSize = next; // Changes the object the caller reaches too.
}

export function rebind(settings: ReportSettings, other: ReportSettings): ReportSettings {
	settings = other; // Changes only which object this parameter reaches.
	return settings;
}
GoAlongside
settings.go
// Three lines that look alike and write to different places.
func ChangeNumber(pageSize, next int) int {
	pageSize = next // Changes only this parameter.
	return pageSize
}

func WriteThrough(settings *ReportSettings, next int) {
	settings.PageSize = next // Changes the struct the caller's pointer reaches too.
}

func Rebind(settings, other *ReportSettings) *ReportSettings {
	settings = other // Changes only which struct this parameter points at.
	return settings
}
Reading the TypeScriptconst, Readonly, and freeze stop different things

const stops a name being reassigned; the object it reaches can still change. Readonly<ReportSettings> stops writes through that type, but not through another name typed without it.

Object.freeze stops writes to the object at runtime, and only to its own properties; an object nested inside stays writable. standard uses both the type and the freeze.

Reading the GoPointers share, structs copy

WriteThrough takes a *ReportSettings, so both sides reach one struct. WithPageSize takes a ReportSettings by value: the struct is copied into the parameter, so editing it can’t affect the caller.

Go has no freeze. Standard stays 25 here because every caller gets a copy, but it’s an exported variable, so any code could still write Standard.PageSize = 99. To rule that out, keep it unexported and hand out copies from a function. A struct that holds a pointer, a slice, or a map still shares whatever that field reaches.

03 / Follow the names

Watch what each name reaches.

Five steps, each running the lesson’s functions on real objects. Names on the left point at objects on the right. A, B, and C label objects in the order they appear, not memory addresses. Before each step, guess which object the line changes.

In Try it, write through a name or point it at another object yourself.

Values and references

What each name reaches, line by line.

A number is copied

Line 1 of 2

const size = saved.pageSize;

A number is copied. After const next = changeNumber(size, 40); saved reaches A, size is 25, next is 40. A has pageSize 25. changeNumber assigned its own parameter. size is still 25, and so is the page size in the settings.

01/ 05
Pass a number and assign the parameter

A number is copied.

Passing size hands the function its own 25. Assigning the parameter leaves the caller’s size alone.

Reduced motion: choose a scene to see its completed state.

Read this scene

Passing size hands the function its own 25. Assigning the parameter leaves the caller’s size alone.

A number is copied. After const next = changeNumber(size, 40); saved reaches A, size is 25, next is 40. A has pageSize 25. changeNumber assigned its own parameter. size is still 25, and so is the page size in the settings.

Watch restarts when you return. Step through keeps your selected step. Try it starts from the same three names each time you open it.

What knowing the difference buys you

Now put names on what you just watched. These are the words you’ll hear in a design review, and each one points at something on this page.

Edits you can predict
A write through a parameter changes the caller’s object; an assignment never does. The line alone tells you which.
Sharing on purpose
The form and the preview agree because they reach one object, not because something keeps two copies in step.
Defaults that stay put
A frozen standard can be handed to every report, and no report can change it for the others.
Cheap change detection
A new object means something changed. React’s memo and Svelte’s raw state rely on exactly that.
Changes the caller chooses
withPageSize returns the change, and the caller decides whether to keep it.

The review words are aliasing for two names reaching one object, pass by value, which still passes the reference, mutation, and immutability for values nobody changes after they’re made. Section 08 covers what they cost.

04 / Try a decision

A default that every report shares.

Someone writes settingsFor so each report starts from the defaults: const settings = defaults;, then the page size. The code is in presets.ts, and the lesson’s tests pin what it returns.

Ana asks for 20 rows a page, then Ben asks for 50. What’s the page size on Ana’s report?

settingsFor starts each report from the shared defaults with const settings = defaults;, sets the page size, and returns the settings.

05 / Give it a real job

Share the open report. Hand out copies of defaults.

In the real report builder, the open report’s settings are one object shared by the form and the preview. Presets and defaults are frozen and never edited. A report that starts from a preset gets its own object.

Open report

Shared on purpose

The form writes and the preview reads the same object.

Presets and defaults

Frozen

Every report starts from a copy.

Rendering

Compares identity

A changed report is a new object, so only its row updates.

The example leaves out nested settings, like a delivery schedule inside a report. A spread copies only the top level, so nested objects stay shared; Copying, identity, and equality follows that further.

Build UIs?Every state update you write chooses between changing an object and replacing it, and one day a memoized list shows you the difference.

Where it already is in your components

React’s documentation is direct. Change a state object in place and “React has no idea that object has changed. So React does not do anything in response.” The textbook React panel replaces the settings object instead.

Svelte takes the other road. $state turns a plain object into “a deeply reactive state proxy”, so writing settings.pageSize is noticed, and the textbook Svelte panel binds straight to it.

When you have to own it

Now it’s a long list of reports whose titles you can edit. In React, each row is wrapped in memo, and “React will compare each prop with Object.is.” Rename a report by writing report.title and the row gets the same object back, so it keeps showing the old title.

In Svelte, the list uses $state.raw, which “cannot be mutated; it can only be reassigned.” Writing report.title changes the object and nothing updates. renameReport fixes both: it replaces only the renamed report, so that row updates and the rest keep their identity.

reports.ts
export type Report = { id: string; title: string; pageSize: number };

// Changing one report makes a new object for it and a new array. Every other report keeps its
// identity, so anything comparing references knows exactly which one changed.
export function renameReport(reports: readonly Report[], id: string, title: string): Report[] {
	return reports.map((report) => (report.id === id ? { ...report, title } : report));
}

A rows-per-page setting: React replaces the settings object, and Svelte binds to a deeply reactive $state object.

ReactAlready in your code
ReportSettingsPanel.tsx
import { useState } from 'react';

type Settings = { pageSize: number; format: 'csv' | 'pdf' };

export function ReportSettingsPanel() {
	const [settings, setSettings] = useState<Settings>({ pageSize: 25, format: 'csv' });

	// settings.pageSize = size would change the object React already has, and React wouldn't know
	// anything changed. Give it a new object instead.
	function changePageSize(size: number) {
		setSettings({ ...settings, pageSize: size });
	}

	return (
		<label>
			Rows per page
			<select
				value={settings.pageSize}
				onChange={(event) => changePageSize(Number(event.target.value))}
			>
				<option value={25}>25</option>
				<option value={50}>50</option>
				<option value={100}>100</option>
			</select>
		</label>
	);
}

06 / Recognize it elsewhere

Anywhere a value crosses a boundary.

You’ve met all of these. For each one, find what’s copied and what’s shared.

Familiar operations, what they copy, and what stays shared
Where you’ve seen itWhat’s copiedWhat stays shared
Passing an object to a functionThe referenceThe object
{ ...settings }The top-level propertiesAny object nested inside
A Go struct passed by valueEvery fieldWhatever a pointer, slice, or map field reaches
React memo propsNothingEach prop, compared with Object.is
Svelte $state.rawNothingThe object; only reassigning is noticed

When two parts of a program see each other’s changes and nobody expected them to, look for two names reaching one object.

07 / Already in your toolbox

Your languages and frameworks already document this.

Three places to look. For each one, find what a write reaches.

React · Updating objects in state

Why state objects should be treated as read-only, what happens when they aren’t, and when mutating is fine: “Mutating an object you’ve just created is okay because no other code references it yet.”

Read the guide ↗

Svelte · $state

Deep proxies, $state.raw for values you only reassign, and $state.snapshot for handing a plain copy to code that doesn’t expect a proxy.

Read the reference ↗

MDN · Functions

The JavaScript guide’s section on function parameters: primitives passed by value, and objects whose properties a function can change for the caller.

Read the guide ↗
A useful counterexample: the live previewWhen sharing is the point

The form and the preview reaching one object is the right design: an edit shows up immediately, with nothing to copy or keep in step. Copying there would mean syncing two objects by hand.

08 / The parts to watch

Every write lands somewhere. Check where.

These are the places it still surprises people.

Reassigning a parameter never reaches the caller

If a function needs the caller to hold a different object, return it and let the caller assign it.

Assigning an object isn’t copying it

const settings = defaults is section 04’s bug: another name, the same object.

const doesn’t freeze

const settings stops settings = other. It doesn’t stop settings.pageSize = 99.

Freeze and spread both stop at one level

Object.freeze protects the object’s own properties, and { ...settings } copies them. An object nested inside is still shared and still writable in both cases.

Go structs copy; their pointers don’t

Passing a struct by value copies its fields. A pointer, slice, or map field still reaches the same data as the original.

09 / Make the call

What would you have to change tomorrow?

Give both approaches a plausible change and follow the work it creates.

How a change affects editing an object in place and returning a new one
The changeEdit the object in placeReturn a new object
The preview must follow the form as it changesFree: both reach one object.The form has to hand each new object to the preview.
Reports start from a shared presetOne report’s edit changes the preset for all.Each report gets its own object.
A memoized row must update on renameSame object, so the row keeps the old title.A new object, so the row updates.
Undo the last changeThe old value is gone.The previous object is still there to go back to.

Reach for a new object whenever someone else may hold the old one and shouldn’t see the change: defaults, presets, and state a renderer compares.

Keep editing in place when every holder is meant to see the change, like a form and its live preview, or an object only one function has touched.

The question I’d leave beside the code is: who else reaches this object, and should they see this write?

10 / Take the idea with you

Explain the preset bug without saying “reference.”

“settings and defaults were two names for the same object, so changing one changed the other. Giving each report its own copy fixed it.” In a review, the words are aliasing, mutation, pass by value, and immutability.

Before moving on, jot down why reassigning the parameter didn’t change the caller, why the next write changed the preset, and one object in your own code that two parts of the program both hold.

Connections to follow nextRelated lessons

Take the settings into your editor. Add a nested delivery schedule, copy a report with a spread, and find the change that still reaches the original.

Back to Concepts & practices →