Repository: sveltejs/kit (js)

Issue:
Accumulate data into `export let data`
### Describe the bug

This is an "opinionated" take on the recent changes to routes and `load` function behavior. It's a big change, that I am not a fan of but can see the merit. Among other things it now requires more boilerplate code, which we all don't like to deal with. 

Here's the problem:  +page.svelte doesn't receive `data` unless there's +page.ts file with `load` function even though we might have `data` set by parent layouts. This forces one to create a bunch of +page.ts files with a the following boilerplate. Wouldn't be nicer to do that as a default for all pages, so we don't have to create +page.ts files unless we're actually doing something specific there? 

```
import type { PageLoad } from './$types';

export const load: PageLoad = async ({ parent }) => {
  return await parent();
};
```
 

### Reproduction

https://stackblitz.com/edit/sveltejs-kit-template-default-f8ky2l?file=src/routes/+page.js

The +page.js file seems to be completely unnecessary but you can't remove it or +page receives no `data`.

### Logs

_No response_

### System Info

```shell
System:
    OS: Linux 5.0 undefined
    CPU: (4) x64 Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
    Memory: 0 Bytes / 0 Bytes
    Shell: 1.0 - /bin/jsh
  Binaries:
    Node: 16.14.2 - /usr/local/bin/node
    Yarn: 1.22.10 - /bin/yarn
    npm: 7.17.0 - /bin/npm
  npmPackages:
    @sveltejs/adapter-auto: next => 1.0.0-next.64 
    @sveltejs/kit: next => 1.0.0-next.416 
    svelte: ^3.46.0 => 3.49.0 
    vite: ^3.0.0 => 3.0.8
```


### Severity

serious, but I can work around it

### Additional Information

_No response_

Pull request:
Accumulate `data`
Closes #5994.

Today, if you have a page with a `load`...

```js
// src/routes/foo/+page.js
export function load() {
  return {
    pagedata: 'hello from page load'
  };
}
```

...then the corresponding `data` prop will contain `pagedata` and nothing else. If you have some data from a layout `load` function that you need in the page, you need to call `await parent()` in the page's `load`:

```diff
// src/routes/foo/+page.js
-export function load() {
+export async function load({ parent }) {
+  const { layoutdata } = await parent();
  return {
+    layoutdata,
    pagedata: 'hello from page load'
  };
}
```

As of this PR, that's no longer the case — layout data will automatically be inherited (with correct types!) so that you can reference `data.layoutdata` (or, more realistically, `data.user.name`) throughout your app without boilerplate.

### Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
- [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
- [x] This message body should clearly illustrate what problems it solves.
- [x] Ideally, include a test that fails without this PR but passes with it.

### Tests
- [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` and `pnpm check`

### Changesets
- [x] If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running `pnpm changeset` and following the prompts. All changesets should be `patch` until SvelteKit 1.0


Patch:
diff --git a/.changeset/heavy-phones-build.md b/.changeset/heavy-phones-build.md
new file mode 100644
index 000000000..8a72eea45
--- /dev/null
+++ b/.changeset/heavy-phones-build.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Accumulate data from parent layouts into `export let data`
diff --git a/documentation/docs/03-routing.md b/documentation/docs/03-routing.md
index 025f6af3c..21ef77f45 100644
--- a/documentation/docs/03-routing.md
+++ b/documentation/docs/03-routing.md
@@ -279,6 +279,18 @@ export function load() {
 
 Unlike `+page.js`, `+layout.js` cannot export `prerender`, `hydrate` and `router`, as these are page-level options.
 
+Data returned from a layout's `load` function is also available to all its child pages:
+
+```svelte
+/// file: src/routes/settings/profile/+page.svelte
+<script>
+	/** @type {import('./$types').PageData} */
+	export let data;
+
+	console.log(data.sections); // [{ slug: 'profile', title: 'Profile' }, ...]
+</script>
+```
+
 > Often, layout data is unchanged when navigating between pages. SvelteKit will intelligently re-run [`load`](/docs/load) functions when necessary.
 
 #### +layout.server.js
diff --git a/packages/kit/src/core/sync/write_types.js b/packages/kit/src/core/sync/write_types.js
index c5cfe1ac5..aa172929e 100644
--- a/packages/kit/src/core/sync/write_types.js
+++ b/packages/kit/src/core/sync/write_types.js
@@ -425,7 +425,7 @@ function process_node(ts, node, outdir, params, groups) {
 			written_proxies.push(write(`${outdir}/proxy${basename}`, proxy.code));
 		}
 
-		server_data = get_data_type(node.server, 'load', 'null', proxy);
+		server_data = get_data_type(node.server, 'null', proxy);
 		server_load = `Kit.ServerLoad<${params}, ${get_parent_type('LayoutServerData')}, OutputData>`;
 
 		if (proxy) {
@@ -449,6 +449,8 @@ function process_node(ts, node, outdir, params, groups) {
 		server_data = 'null';
 	}
 
+	const parent_type = get_parent_type('LayoutData');
+
 	if (node.shared) {
 		const content = fs.readFileSync(node.shared, 'utf8');
 		const proxy = tweak_types(ts, content, shared_names);
@@ -456,29 +458,32 @@ function process_node(ts, node, outdir, params, groups) {
 			written_proxies.push(write(`${outdir}/proxy${path.basename(node.shared)}`, proxy.code));
 		}
 
-		data = get_data_type(node.shared, 'load', server_data, proxy);
-		load = `Kit.Load<${params}, ${server_data}, ${get_parent_type('LayoutData')}, OutputData>`;
+		const type = get_data_type(node.shared, `${parent_type} & ${server_data}`, proxy);
+
+		data = `Omit<${parent_type}, keyof ${type}> & ${type}`;
+		load = `Kit.Load<${params}, ${server_data}, ${parent_type}, OutputData>`;
+	} else if (server_data === 'null') {
+		data = parent_type;
 	} else {
-		data = server_data;
+		data = `Omit<${parent_type}, keyof ${server_data}> & ${server_data}`;
 	}
 
 	return { data, server_data, load, server_load, errors, written_proxies };
 
 	/**
 	 * @param {string} file_path
-	 * @param {string} method
 	 * @param {string} fallback
 	 * @param {Proxy} proxy
 	 */
-	function get_data_type(file_path, method, fallback, proxy) {
+	function get_data_type(file_path, fallback, proxy) {
 		if (proxy) {
-			if (proxy.exports.includes(method)) {
+			if (proxy.exports.includes('load')) {
 				// If the file wasn't tweaked, we can use the return type of the original file.
 				// The advantage is that type updates are reflected without saving.
 				const from = proxy.modified
 					? `./proxy${replace_ext_with_js(path.basename(file_path))}`
 					: path_to_original(outdir, file_path);
-				return `Kit.AwaitedProperties<Awaited<ReturnType<typeof import('${from}').${method}>>>`;
+				return `Kit.AwaitedProperties<Awaited<ReturnType<typeof import('${from}').load>>>`;
 			} else {
 				return fallback;
 			}
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index f500b14d3..08a0e2de0 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -403,10 +403,10 @@ export function create_client({ target, base, trailing_slash }) {
 		let data = {};
 		let data_changed = false;
 		for (let i = 0; i < filtered.length; i += 1) {
-			Object.assign(data, filtered[i].data);
+			data = { ...data, ...filtered[i].data };
 			// Only set props if the node actually updated. This prevents needless rerenders.
-			if (!current.branch.some((node) => node === filtered[i])) {
-				result.props[`data_${i}`] = filtered[i].data;
+			if (data_changed || !current.branch.some((node) => node === filtered[i])) {
+				result.props[`data_${i}`] = data;
 				data_changed = true;
 			}
 		}
diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js
index 019229d13..4b8a93508 100644
--- a/packages/kit/src/runtime/server/page/render.js
+++ b/packages/kit/src/runtime/server/page/render.js
@@ -83,18 +83,31 @@ export async function render_response({
 				navigating: writable(null),
 				updated
 			},
-			/** @type {import('types').Page} */
-			page: {
-				error,
-				params: /** @type {Record<string, any>} */ (event.params),
-				routeId: event.routeId,
-				status,
-				url: state.prerendering ? new PrerenderingURL(event.url) : event.url,
-				data: branch.reduce((acc, { data }) => (Object.assign(acc, data), acc), {})
-			},
 			components: await Promise.all(branch.map(({ node }) => node.component()))
 		};
 
+		let data = {};
+
+		// props_n (instead of props[n]) makes it easy to avoid
+		// unnecessary updates for layout components
+		for (let i = 0; i < branch.length; i += 1) {
+			data = { ...data, ...branch[i].data };
+			props[`data_${i}`] = data;
+		}
+
+		props.page = {
+			error,
+			params: /** @type {Record<string, any>} */ (event.params),
+			routeId: event.routeId,
+			status,
+			url: state.prerendering ? new PrerenderingURL(event.url) : event.url,
+			data
+		};
+
+		if (validation_errors) {
+			props.errors = validation_errors;
+		}
+
 		// TODO remove this for 1.0
 		/**
 		 * @param {string} property
@@ -112,16 +125,6 @@ export async function render_response({
 		print_error('path', 'pathname');
 		print_error('query', 'searchParams');
 
-		// props_n (instead of props[n]) makes it easy to avoid
-		// unnecessary updates for layout components
-		for (let i = 0; i < branch.length; i += 1) {
-			props[`data_${i}`] = branch[i].data;
-		}
-
-		if (validation_errors) {
-			props.errors = validation_errors;
-		}
-
 		rendered = options.root.render(props);
 
 		for (const { node } of branch) {


Test patch:
diff --git a/packages/kit/test/apps/basics/src/routes/load/accumulated/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/accumulated/+page.svelte
new file mode 100644
index 000000000..7a8b04d34
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/accumulated/+page.svelte
@@ -0,0 +1,2 @@
+<a href="/load/accumulated/with-page-data">with page data</a>
+<a href="/load/accumulated/without-page-data">without page data</a>
diff --git a/packages/kit/test/apps/basics/src/routes/load/accumulated/with-page-data/+page.js b/packages/kit/test/apps/basics/src/routes/load/accumulated/with-page-data/+page.js
new file mode 100644
index 000000000..b02a83c36
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/accumulated/with-page-data/+page.js
@@ -0,0 +1,6 @@
+/** @type {import('./$types').PageLoad} */
+export function load() {
+	return {
+		pagedata: 'pagedata'
+	};
+}
diff --git a/packages/kit/test/apps/basics/src/routes/load/accumulated/with-page-data/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/accumulated/with-page-data/+page.svelte
new file mode 100644
index 000000000..c2cb10840
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/accumulated/with-page-data/+page.svelte
@@ -0,0 +1,7 @@
+<script>
+	/** @type {import('./$types').PageData} */
+	export let data;
+</script>
+
+<h1>foo.bar: {data.foo.bar}</h1>
+<h2>pagedata: {data.pagedata}</h2>
diff --git a/packages/kit/test/apps/basics/src/routes/load/accumulated/without-page-data/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/accumulated/without-page-data/+page.svelte
new file mode 100644
index 000000000..277d65c68
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/accumulated/without-page-data/+page.svelte
@@ -0,0 +1,6 @@
+<script>
+	/** @type {import('./$types').PageData} */
+	export let data;
+</script>
+
+<h1>foo.bar: {data.foo.bar}</h1>
diff --git a/packages/kit/test/apps/basics/src/routes/load/layout-props/+layout.js b/packages/kit/test/apps/basics/src/routes/load/layout-props/+layout.js
deleted file mode 100644
index 383bdc9c2..000000000
--- a/packages/kit/test/apps/basics/src/routes/load/layout-props/+layout.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/** @type {import('./$types').LayoutLoad} */
-export async function load() {
-	return {
-		// Needs to be an object, else Svelte will do by-value-comparison and skip rerender
-		obj: {}
-	};
-}
diff --git a/packages/kit/test/apps/basics/src/routes/load/layout-props/+layout.svelte b/packages/kit/test/apps/basics/src/routes/load/layout-props/+layout.svelte
deleted file mode 100644
index d7d62c04d..000000000
--- a/packages/kit/test/apps/basics/src/routes/load/layout-props/+layout.svelte
+++ /dev/null
@@ -1,10 +0,0 @@
-<script>
-	/** @type {import('./$types').LayoutData} */
-	export let data;
-
-	let count = 0;
-	$: data.obj && count++;
-</script>
-
-<h1>{count}</h1>
-<slot />
diff --git a/packages/kit/test/apps/basics/src/routes/load/layout-props/a/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/layout-props/a/+page.svelte
deleted file mode 100644
index 77fcba3de..000000000
--- a/packages/kit/test/apps/basics/src/routes/load/layout-props/a/+page.svelte
+++ /dev/null
@@ -1,2 +0,0 @@
-<h2>On a</h2>
-<a href="/load/layout-props/b">to b</a>
diff --git a/packages/kit/test/apps/basics/src/routes/load/layout-props/b/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/layout-props/b/+page.svelte
deleted file mode 100644
index cb5b54229..000000000
--- a/packages/kit/test/apps/basics/src/routes/load/layout-props/b/+page.svelte
+++ /dev/null
@@ -1,2 +0,0 @@
-<h2>On b</h2>
-<a href="/load/layout-props/a">to a</a>
diff --git a/packages/kit/test/apps/basics/src/routes/load/props/+layout.svelte b/packages/kit/test/apps/basics/src/routes/load/props/+layout.svelte
deleted file mode 100644
index 9423a8777..000000000
--- a/packages/kit/test/apps/basics/src/routes/load/props/+layout.svelte
+++ /dev/null
@@ -1,10 +0,0 @@
-<header>
-	<nav>
-		<a href="/load/props/">Index</a>
-		<a href="/load/props/about">About</a>
-	</nav>
-</header>
-
-<main>
-	<slot />
-</main>
diff --git a/packages/kit/test/apps/basics/src/routes/load/props/about/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/props/about/+page.svelte
deleted file mode 100644
index ee6be11dc..000000000
--- a/packages/kit/test/apps/basics/src/routes/load/props/about/+page.svelte
+++ /dev/null
@@ -1,7 +0,0 @@
-<script>
-	export let data = 'this default is always ignored';
-</script>
-
-<h3>About</h3>
-
-<p>Data: {data}</p>
diff --git a/packages/kit/test/apps/basics/src/routes/shadowed/parent/+layout.svelte b/packages/kit/test/apps/basics/src/routes/shadowed/parent/+layout.svelte
index 5a30dcf36..931dbd597 100644
--- a/packages/kit/test/apps/basics/src/routes/shadowed/parent/+layout.svelte
+++ b/packages/kit/test/apps/basics/src/routes/shadowed/parent/+layout.svelte
@@ -1,5 +1,5 @@
 <script>
-    export let data;
+	export let data;
 </script>
 
 <h2>Layout data: {JSON.stringify(data)}</h2>
diff --git a/packages/kit/test/apps/basics/src/routes/shadowed/parent/+page.svelte b/packages/kit/test/apps/basics/src/routes/shadowed/parent/+page.svelte
index 10ec5756a..90658ee10 100644
--- a/packages/kit/test/apps/basics/src/routes/shadowed/parent/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/shadowed/parent/+page.svelte
@@ -1,5 +1,5 @@
 <script>
-    export let data;
+	export let data;
 </script>
 
 <p>Page data: {JSON.stringify(data)}</p>
diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js
index b8edb7e4f..05be5cf42 100644
--- a/packages/kit/test/apps/basics/test/client.test.js
+++ b/packages/kit/test/apps/basics/test/client.test.js
@@ -360,13 +360,6 @@ test.describe('Load', () => {
 		expect(await page.textContent('h1')).toBe("I didn't break!");
 	});
 
-	test("layout props don't cause rerender when unchanged", async ({ page, clicknav }) => {
-		await page.goto('/load/layout-props/a');
-		expect(await page.textContent('h1')).toBe('1');
-		await clicknav('[href="/load/layout-props/b"]');
-		expect(await page.textContent('h1')).toBe('1');
-	});
-
 	if (process.env.DEV) {
 		test('using window.fetch causes a warning', async ({ page }) => {
 			const port = 5173;
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js
index 1a0f1a553..1407da0ae 100644
--- a/packages/kit/test/apps/basics/test/test.js
+++ b/packages/kit/test/apps/basics/test/test.js
@@ -260,21 +260,27 @@ test.describe('Shadowed pages', () => {
 
 	test('Parent data is present', async ({ page, clicknav }) => {
 		await page.goto('/shadowed/parent');
-		await expect(page.locator('h2')).toHaveText('Layout data: {"layout":"layout"}');
+		await expect(page.locator('h2')).toHaveText(
+			'Layout data: {"foo":{"bar":"Custom layout"},"layout":"layout"}'
+		);
 		await expect(page.locator('p')).toHaveText(
-			'Page data: {"page":"page","data":{"rootlayout":"rootlayout","layout":"layout"}}'
+			'Page data: {"foo":{"bar":"Custom layout"},"layout":"layout","page":"page","data":{"rootlayout":"rootlayout","layout":"layout"}}'
 		);
 
 		await clicknav('[href="/shadowed/parent?test"]');
-		await expect(page.locator('h2')).toHaveText('Layout data: {"layout":"layout"}');
+		await expect(page.locator('h2')).toHaveText(
+			'Layout data: {"foo":{"bar":"Custom layout"},"layout":"layout"}'
+		);
 		await expect(page.locator('p')).toHaveText(
-			'Page data: {"page":"page","data":{"rootlayout":"rootlayout","layout":"layout"}}'
+			'Page data: {"foo":{"bar":"Custom layout"},"layout":"layout","page":"page","data":{"rootlayout":"rootlayout","layout":"layout"}}'
 		);
 
 		await clicknav('[href="/shadowed/parent/sub"]');
-		await expect(page.locator('h2')).toHaveText('Layout data: {"layout":"layout"}');
+		await expect(page.locator('h2')).toHaveText(
+			'Layout data: {"foo":{"bar":"Custom layout"},"layout":"layout"}'
+		);
 		await expect(page.locator('p')).toHaveText(
-			'Page data: {"sub":"sub","data":{"rootlayout":"rootlayout","layout":"layout"}}'
+			'Page data: {"foo":{"bar":"Custom layout"},"layout":"layout","sub":"sub","data":{"rootlayout":"rootlayout","layout":"layout"}}'
 		);
 	});
 
@@ -842,15 +848,6 @@ test.describe('Load', () => {
 		expect(await page.innerHTML('.raw')).toBe('{ "oddly" : { "formatted" : "json" } }');
 	});
 
-	test('does not leak props to other pages', async ({ page, clicknav }) => {
-		await page.goto('/load/props/about');
-		expect(await page.textContent('p')).toBe('Data: null');
-		await clicknav('[href="/load/props/"]');
-		expect(await page.textContent('p')).toBe('Data: Hello from Index!');
-		await clicknav('[href="/load/props/about"]');
-		expect(await page.textContent('p')).toBe('Data: null');
-	});
-
 	test('server-side fetch respects set-cookie header', async ({ page, context }) => {
 		await context.clearCookies();
 
@@ -878,6 +875,21 @@ test.describe('Load', () => {
 			})
 		).toBe('rgb(255, 0, 0)');
 	});
+
+	test('page without load has access to layout data', async ({ page, clicknav }) => {
+		await page.goto('/load/accumulated');
+
+		await clicknav('[href="/load/accumulated/without-page-data"]');
+		expect(await page.textContent('h1')).toBe('foo.bar: Custom layout');
+	});
+
+	test('page with load has access to layout data', async ({ page, clicknav }) => {
+		await page.goto('/load/accumulated');
+
+		await clicknav('[href="/load/accumulated/with-page-data"]');
+		expect(await page.textContent('h1')).toBe('foo.bar: Custom layout');
+		expect(await page.textContent('h2')).toBe('pagedata: pagedata');
+	});
 });
 
 test.describe('Method overrides', () => {