Don't Put Your Data In React
Tags: react, javascript, realy, data
Published: 2022-08-14 13:00:29

We've all got applications with views. You know, views, the macro components we use to compose together to build our pages. These pages then tie in with our routes to build our applications.

This post will walk you through creating something that resembles Relay EntryPoints, what that means for these views. And how it's not really a story about user interfaces, but a story about where your data lives.

### Enter EntryPoint's

We have a layout of views, a collection we love to call pages. But let's think for a moment, what is that really?

In React we like to think we are composing components, you know, the thing you put inside off another thing[^1]. So let's riff of that for a second.

[^1]: No not the John Cleese [Royal Society For Putting Things On Top of Other Things](https://youtu.be/LFrdqQZ8FFc) because that would be rather silly.

![our app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zi7lzud5j9vd1wl9lkdf.png)
 
We can see we have something that looks like GitHub:

- the global nav, persistent across multiple pages
- the project header, persistent across multiple project tabs
- the code tab, or the "main content"

All views or components that when composed together build up the repository page. Our page composed all those together into a single _root_ node we're going to call the `RepositoryView`.

Before terminology gets out of hand, let's instead refer to it as "composing layouts of entry points".

So our entry point here would be the page itself, the `RepositoryViewEntryPoint`, a `ProjectHeaderEntryPoint` and of course the main content as well.

All these things are the building blocks of our application — an "entry point" into a piece of code (its view) and it's data.

Let's unpack what Entry Points are, and come full circle towards the end as we build GitHub.

## EntryPoints are type-safe

Before we get into how Relay handles this, let's build our own! 😅

There is really 3 parts that go into making an entry point.

1. the type definition of what an entry point even is
2. a component
3. and an entry point code to that component

```tsx
// 1. the type definition
type EntryPoint<Props> = {
	component: ComponentType<Props>,
	props: Props
};

// 2. the component
type Props = { user: { name: string } };
const Howdy: SFC<Props> = ({ user }) => (
	<div>Hello {user.name}</div>
);

// 3. the entry point 
const HowdyEntryPoint: EntryPoint<Props> = {
	component: Howdy,
	props: { user: { name: 'Mona' } },
};
```

... and now you think I've completely lost it! 😅 "You've just put the components props next to something that already defines it" ... but bare with me.

What we've done here is, we've established a _container_ creating a type-safe dependency between the component and it's data.

Don't let me understate that, one of the powerful aspects of components especially with the help of Flow and TypeScript is the ability to define component prop types. So as we venture into this notion of "Don't Put Data in React", we need to retain this type-safe aspect.

If the component requires a new set of data requirements, since we've established this type-safe dependency between them, you won't forget to also give those new data to the component — your type-checker will have a whinge.

But how have we moved the data out of React? Really in the literal sense: `<Howdy user={{ name: 'mona' }} />`, has the `{ name: 'mona' }` as data declare _in-react_. So we've moved the data alongside a reference to the component, in the form of a joining object, the `EntryPoint`.

Great! Let's render this to the screen, which would happens as you'd might expect:

```tsx
const EntryPointContainer: SFC<{ entrypoint: EntryPoint<unknown> }> = ({
	entrypoint: {
		component: Component,
		props,
	},
}) => (
	<Component {...props} />
);

<EntryPointContainer entrypoint={HowdyEntryPoint} />
```

The `EntryPointContainer` here takes a reference to an entry point, and wires ups the props and renders.

## EntryPoints describe the data dependency

Now! Wouldn't the world be wonderful if all we ever had was static data. If we did, the post would stop here 😂! Instead we live in a world where our data lives in remote places, databases, apis, your great aunts show box.

So let's reframe our mindset a little, instead of asking "what data goes with this component" lets ask "what query do I run to get the data for this component".

An entry point describes the data dependency.

Well, what does it mean to _describe_?

> _to give a report of how something is done_ — [Cambridge](https://dictionary.cambridge.org/dictionary/english/describe)

Notice how it's "how something is done", not "what the something is".

In terms of software, how do we describe how data is done, or fetched? Through a function perhaps? A function describes how data is resolved, not it's result.

Let's describe the data dependency and change our example to reflect this:

```tsx
type EntryPoint<Variables, Props> = {
	component: ComponentType<Props>,
	fetch: (variables: Variables) => Promise<Props>,
	variables: Variables
};

const HowdyEntryPoint: EntryPoint<{ userId: string }, Props> = {
	component: Howdy,
	fetch(variables) {
		return fetchGraphql(graphql`query($id: ID) { user(id: $id) { name }}`);
	},
	variables: { userId: 2 },
};
```

Instead of passing the `props` we had statically before. We define a describing function on how to resolve the data, in our case by calling some api. As most functions do, they can accept some input to make it configurable, let's expose that by way of `variables`.

For the purposes of this post, use your imagination as to where to get those `variables` from, but could be something like `useParams` from your favourite routing library.

Our `EntryPointContainer` component also needs to be altered a little to handle this new `fetch` and `variables` properties.

```tsx
const EntryPointContainer: SFC<{ entrypoint: EntryPoint<unknown, unknown> }> = ({
	entrypoint: {
		component: Component,
		fetch: fetchQuery,
		variables,
	},
}) => {
	const [props, setProps] = useState(null);

	useEffect(() => {
		fetchQuery(variables)
			.then(props => {
				setProps(props);
			});
	}, [fetch, variables]);

	if (props === null) return null;

	return <Component {...props} />;
};
```

Simple stuff, a `useEffect`[^2] to call our fetch function and only rendering the `Component` once that data comes back.

... and the usage stays the same!

```tsx
<EntryPointContainer entrypoint={HowdyEntryPoint} />
```

[^2]: Don't use this in production for [reasons](https://youtu.be/HPoC-k7Rxwo), and for things error boundaries, loading states and so forth.

We can actually go one step further. We all use GraphQL around these parts. So instead of passing a fetch function, let's describe the data by way of a GraphQL query 🦸‍♂️.

```tsx
type EntryPoint<Variables, Props> = {
	component: ComponentType<Props>,
	query: string,
	variables: Variables
};

const HowdyEntryPoint: EntryPoint<{ userId: string }, Props> = {
	component: () => import('./howdy'),
	query: /* GraphQL */`query($id: ID) { user(id: $id) { name }}`,
	variables: { userId: 2 },
};
```

### Necessary encapsulation

What we did just then was describe the data dependency as something high-level, a GraphQL query. Which I cannot overstate as quite pivotal moment in our understanding of entry points. 

We've moved the _platform_ layer, the fetch function into a describing factor, leaving our platform engineers free to enact that fetch logic in our behalf, and at their pace.

I said before "a function describes how data is resolved, not it's result", but the problem with functions is they are heavy — often coupled to some network layer, so carries too much definition. 

## EntryPoints describe the ui dependency

Great! Our entry points can now mount and data is described.

But hang on... We've still got a synchronous bundle of the code. There is probably an entire article for this moment entirely.

If we continue down this notion of entry points being describing containers, we need to describe our component then as well — it's still _the data_ not _describing the data_.

So let's fix that...

And what better way to do this than with our trusty esm import functions.

```tsx
type EntryPoint<Variables, Props> = {
	component: () => Promise<ComponentType<Props>>,
	query: string,
	variables: Variables
};

const EntryPointContainer: SFC<{ entrypoint: EntryPoint<unknown, unknown> }> = ({
	entrypoint: {
		component,
		query,
		variables,
	},
}) => {
	const [props, setProps] = useState(null);
	const [Component, setComponent] = useState(null);

	useEffect(() => {
		fetchQuery(query, variables)
			.then(props => {
				setProps(props);
			});
	}, [query, variables]);

	useEffect(() => {
		component()
			.then(Component => {
				setComponent(Component);
			});
	}, [component]);

	if (props === null || Component === null) return null;

	return <Component {...props} />;
};
```

... the component and the data is both split away, creating a thin **_json serialisable_**[^3] definition of how to _paint_ this entry point 🦄.

[^3]: Just need to move our async import into a string that is looked-up/fetched similar to how the query is. `JSResource` will be your friend.

Need to quickly fix our `HowdyEntryPoint` to use these new properties:

```tsx
const HowdyEntryPoint: EntryPoint<{ userId: string }, Props> = {
	component: () => import('./howdy'),
	query: /* GraphQL */`query($id: ID) { user(id: $id) { name }}`,
	variables: { userId: 2 },
};
```

... and it all still renders the same!

✨ You've made it! Congratulations 🎉, you have built Relay Entry Points!

We've taken what was "code" into what is now a "description"!

There's just one thing... Great! We've moved the data out of React and how, but why?

## Why data doesn't belong in React

If we switch gears to looking at this question from the point of view of a server needing to prepare the data required for a page.

If all the data was _in-react_ (noting it's not static as mentioned before), how would it know what data to go and prepare? We'd need to render the entire React tree to discover these things, which is quite a costly endeavour.

There's an entire article on this topic, and how routing might work. But to help me with this article, let's just say routes point to entry points. So when the server receives a request for a route, we can look at all our entry points, grab the one that matched.

So we have static/instant access to the data requirements — and how to get it.

That's it, that's why! Gee Marais, took you long enough!

Let's continue looking at how we can solve this. The clever ones might have realised, our application topology originally described global navs, main contents, project headers etc.. If those are all "Entry Points" we composed.

We'd get some pretty nasty waterfall loadings 😭, so let's fix that!

## EntryPoints describe the rendering and loading

We're getting into the weeds now with respects to [resource loading, and Suspense](https://17.reactjs.org/docs/concurrent-mode-suspense.html#what-is-suspense-exactly) have a gander probably firstly. Super tl;dr — suspense is a way for React to handle promises for us.

In the first example we had our data right there available, 👋 mona. All we needed was `{ props: { data } }`, and done. Now we've got this intermediate loading state, api shenanigans to deal with.

Wouldn't it be nice if we could take our definitions of entry points, and frame them back into a form where the data was static.

Let's try!

What immediately comes to mind is loading the data before we render:

```tsx
// Something suspensey
type PreloadedEntryPoint<Data> = { ... };

const loadEntryPoint = <Variables, Props>(
	entrypoint: EntryPoint<Variables, Props>,
	variables: Variables,
): Promise<PreloadedEntryPoint<Props>> => { ... };

const EntryPointContainer: SFC<{ entrypoint: PreloadedEntryPoint<unknown> }> = ({
	entrypoint,
}) => {
	const { Component, props } = entrypoint.read(); // suspends

	return <Component {...props} />;
};

loadEntryPoint(HowdyEntryPoint)
	.then(entrypoint => {
		ReactDOM.render(<EntryPointContainer entrypoint={entrypoint} />);
	});
```

Much like our type-safe dependency we created with our Entry Point. We've created another layer of type-safety, joining an in-flight or preloaded type-safe container of the data to the entry point. This ensures we are passing around right preloaded data to the right component.

Now, you're thinking. We have to be explicitly be passing around these preloaded data containers, and wondering why.

It's actually a really good thing. If it quack like a duck, swims like a duck then call it a duck. It makes it quite clear who needs it, whose using it and of course when nobody uses anymore, it's safe to delete.

Our component doesn't need the definition of the data, it needs the data itself! So from the components point of view it effetely voices "hey I need this preloaded data", which answers the "who needs it" question. 

The "who is using it" question, is tracked by way of passing that into the `EntryPointContainer` component. We're not going to get into Relay's concept of reference counting, but the idea is when the preloaded data is no longer used, we can omit this from our memory. Because it's safe to do. For if we need it again, we know how to get it again.

... and bam! You've achieved the Relay definition of entry points.

Let's see one, and build GitHub!

## EntryPoints build GitHub[^4]

[^4]: None of this is actually how GitHub is built, nor endorsed or sponsored by them.

As much as we loved our `Howdy` component, let's define something real like you'd expect to see.

{% collapsible ProjectHeader %}

```tsx
const ProjectHeader: SFC<{
	queries: {
		queryRef: PreloadedQuery<typeof ProjectHeaderQuery>
	}
}> = ({ queries }) => {
	const data = usePreloadedQuery(graphql`query ProjectHeaderQuery($owner: String, $repo: String) {
		repository(owner: $owner, name: $repo) {
			owner
			name
			stars
		}
	}`, queries.queryRef);

	return <div>
		<h1>{data.repository.owner}/{data.repository.name}</h1>
		<button>Stars {data.repository.stars}</button>
	</div>;
};

const ProjectHeaderEntryPoint: EntryPoint<{
	owner: string,
	repo: string
}> = {
	root: JSResource('ProjectHeader'),
	getPreloadedProps(params) {
		return {
			queries: {
				queryRef: {
					parameters: ProjectHeaderQuery,
					variables: {
						owner: params.owner,
						user: params.repo,
					},
				},
			},
		};
	},
};
```
{% endcollapsible %}

{% collapsible RepositoryView %}

```tsx
const RepositoryView: SFC<{
	queries: {
		queryRef: PreloadedQuery<typeof RepositoryViewQuery>
	},
	entryPoints: {
		projectHeader: typeof ProjectHeaderPoint
	}
}> = ({ queries, entrypoints }) => {
	const data = usePreloadedQuery(graphql`query RepositoryViewQuery($owner: String, $repo: String) {
		repository(owner: $owner, name: $repo) {
			readme {
				html
			}
		}
	}`, queries.queryRef);

	return <div>
		<EntryPointContainer entrypoint={entrypoints.projectHeader}/>

		<div>
			<h2>Readme</h2>
			<div dangerouslySetInnerHTML={{ __html: data.repository.readme.html }}/>
		</div>
	</div>;
};

const RepositoryViewEntryPoint: EntryPoint<{
	owner: string,
	repo: string
}> = {
	root: JSResource('RepositoryView'),
	getPreloadedProps(params) {
		return {
			queries: {
				queryRef: {
					parameters: RepositoryViewQuery,
					variables: {
						owner: params.owner,
						user: params.repo,
					},
				},
			},
			entryPoints: {
				projectHeader: ProjectHeaderEntryPoint,
			},
		};
	},
};
```

{% endcollapsible %}

Have a read of those, but our app would compose them into something like this:

> Please don't crucify me, please do handle errors, edge cases, etc.. It's pseudo code at best.

```tsx
let routes = {
	'/:owner/:repo': RepositoryViewEntryPoint,
};

const matchRoute = (url: string) => routes[url];

const initialPage = loadEntryPoint(matchRoute(location.href));

const App = () => {
	const { entrypoint, setEntryPoint } = useState(initialPage);

	useEffect(() => {
		// Please use something like https://github.com/lukeed/navaid
		window.addEventListener('pushstate', () => {
			setEntryPoint(matchRoute(location.href));
		});
	}, []);

	return <Suspense fallback={null}>
		<EntryPointContainer entrypoint={entrypoint}/>
	</Suspense>;
};
```

Wowzers! EntryPoints can compose other EntryPoints!!?!?!

Our project header is composed by the repository view (or page or layout), similar to the [`Outlet` concept](https://reactrouter.com/docs/en/v6/components/outlet#outlet).

Relay when that top level EntryPoint is `loadEntrypoint`ed, will recursively call the `getPreloadedProps`, and all the data and code fetchers will all run in parallel.

## Modals

... or really anything behind a user interaction — is an EntryPoint.

Seeing as the "building block" is described as an entry point we can preload, or deferred load this behind user interaction.

Like say the GitHub "code fly out", the fly out there requires — the users codespaces, the ssh or html preference, and potentially all sorts of other ui and data, which is not required for the critical load.

We can then declare this as an EntryPoint like so:

```tsx
const CodeFlyout: SFC<{
	queries: {
		queryRef: PreloadedQuery<typeof CodeFlyoutQuery>
	}
}> = ({ queries }) => {
	const data = usePreloadedQuery(graphql`query CodeFlyoutQuery($owner: String, $repo: String) {
		repository(owner: $owner, name: $repo) {
			url {
				ssh
				https
			}

			codespaces {
				name
				url
			}
		}

		viewer {
			cloning_preference
		}
	}`, queries.queryRef);

	return (<div>
		<Tabs active={data.viewer.cloning_preference}>
			<Item name="ssh">
				<pre>{data.repository.url.ssh}</pre>
			</Item>
			<Item name="https">
				<pre>{data.repository.url.https}</pre>
			</Item>
		</Tabs>

		<p>Codespaces is awesome, you should use it</p>
		{data.repository.codespaces.map(item => (
			<a href={item.url}>Open codespace {item.name}</a>
		))}
	</div>);
};

const CodeFlyoutEntryPoint: EntryPoint<{
	owner: string,
	repo: string
}> = {
	root: JSResource('CodeFlyout'),
	getPreloadedProps(params) {
		return {
			queries: {
				queryRef: {
					parameters: CodeFlyoutQuery,
					variables: {
						owner: params.owner,
						user: params.repo,
					},
				},
			},
		};
	},
};

const RepositoryView = () => {
	return (<div>
		{ /* all the other stuff from above */}

		<FlyoutTrigger entrypoint={CodeFlyoutEntryPoint}>
			{({ onClick }) =>
				(<button onClick={onClick}>Code</button>)
			}
		</FlyoutTrigger>
	</div>);
};
```

Just wonderful, we've declaratively composed what our page needs, its all feeling great from a UX point of view. The bits that sit behind user interaction are code-split and everything is great! And best of all, its type-safe through and through!!!

But really skies the limit now in how you use it!

- you could preload the entry point on hover
- you could `intersection observer` to check that all visible ModalTrigers have their entry points preloaded

## EntryPoints can protect your routes

Note that `routes` object above can come from a window object, incrementally hydrated from an api or whatever — its just json.

A side moment, and something important ☝️.

To handle permissions, read access, and discoverability of routes. You may not want to flush your entire entry point map to the client. But instead before a navigation to a route occurs, you ask the server for the entry point json - or not return anything for like a 404.

You could do something like:

```tsx
useEffect(() => {
	window.addEventListener('pushstate', () => {
		const target = location.href;
		fetch(`/routes?to=${target}`)
			.then(route => {
				if (route) {
					Object.assign(routes, route);
					setEntryPoint(matchRoute(target));
				} else {
					setEntryPoint(matchRoute('404'));
				}
			});
	});
}, []);
```

... please write something better than that, but the idea is. Either on hover, or on click — first ask your well protected backend what the entry point is to power that route.

If it returns nothing, then 404. If it returns, go for it. Meaning all the "this user can access it" etc can all be surfaced hiding all the usual, "the route exists but the user can't see it" security risks.

Think like a private repo, if the entry point exists, and was attempted, then maybe you can use that to try other things.

---

## Summary

Let's quickly recap what we've achieved and make sure you've grasped the main points.

1. entry points are thin json serializable definitions of, what code to run, and the data that code may need.
2. entry points describe the data dependency, not the data itself.
3. entry points describe the code dependency.
4. entry points are type-safe and statically analysable.
5. entry points are loaded and handled outside the react life cycle.
6. entry points should wrap things that sit behind user interaction, route transitions are behind user interaction.

## Read More

#### What is `JSResource`?

Quite simply just function that returns a suspenseful wrapper around a promise. Remember before when I said entry points are json serialisable, well this is how. `JSResource` under the hood, would be going `import('./components/${name}')`. Or however you wish to resolve it.

Sample implementation 👉 [`npm jsr`](https://unpkg.com/browse/jsr@0.5.1/dist/index.js)

### Thanks

Special thanks to [Tom Gasson](https://twitter.com/tomgasson) for article inspiration ❤️

Cover photo by Ivan Aleksic

---

Follow me on twitter ~> [@slightlycode](https://twitter.com/slightlycode)