Store normalization
Store normalization allows you to have a single source of truth for all your data.
Benefits can be:
- improved memory usage, because only minimal data is stored
- preventing extra requests by using a centralized cooldown
- simplify your code by only using elements instead of containers
XSWR supports any data structure for normalization, with any level of nesting, without any extra dependency.
You can give it an already normalized structure (e.g. with normalizr), or a simple array, or a deeply nested, GraphQL-like data structure with duplicated data everywhere.
It just works!
Usage
You just have to define a normalizer function that will take your data and return the same data but normalized.
async function normalizer(data: Data, more: NormalizerMore) {
// Do some normalization
return { ...data }
}
You can use await schema.normalize(data, more)
to recursively normalize a schema with the given data.
async function getDataRef(data: Data | Ref, more: NormalizerMore) {
if ("ref" in data) return data // don't normalize a ref
const schema = getDataSchema(data.id) // grab the schema of this item
await schema.normalize(data, more) // mutate it in storage and apply recursive normalization
return { ref: true, id: data.id } as Ref // return a reference to it
}
async function normalizer(data: Data[], more: NormalizerMore) {
return await Promise.all(data.map(it => getDataRef(it, more)))
}
Complete example with an array
We'll use normalization for an array that contains items of type Data, each with an unique id
interface Data {
id: string
name: string
}
Let's define a reference to it
interface Ref {
ref: true // type checker
id: string
}
First, create a schema factory for an item
function getDataSchema(id: string) {
return getSchema<Data>(`/api/data?id=${id}`, fetchAsJson)
}
Then, create a ref factory for an item
A normal is an object that encapsulates your data, its schema, and a reference to your data (so we can delete the original data and just keep the reference)
async function getDataRef(data: Data | Ref, more: NormalizerMore) {
if ("ref" in data) return data // don't normalize a ref
const schema = getDataSchema(data.id) // grab the schema of this item
await schema.normalize(data, more) // mutate it in storage and apply recursive normalization
return { ref: true, id: data.id } as Ref // return a reference to it
}
Then, create a schema for your container, and create a normalizer, it will return the new structure of your container
In this case, all the array is mapped to normals, which will then automatically be replaced by references by XSWR
function getAllDataSchema() {
async function normalizer(data: Data[], more: NormalizerMore) {
return await Promise.all(data.map(it => getDataRef(it, more)))
}
return getSchema<(Data | Ref)[]>(
`/api/data/all`,
fetchAsJson,
{ normalizer })
}
Notice the modified type parameter (Data | Ref)[]
, it means our container can hold both data and references
Then create an element component
function useData(id: string) {
return useSchema(getDataSchema, [id])
}
function Element(props: { id: string }) {
const { data } = useData(id)
return <div>{JSON.stringify(data)}</div>
}
And a container component
function useAllData() {
const query = useSchema(getAllDataSchema, [])
useFetch(query)
return query
}
function Container() {
const { data } = useAllData()
return <>
{data?.map(dataOrRef =>
<Element
key={dataOrRef.id}
id={dataOrRef.id} />)}
</>
}
That's it! You can find a full working example in the array test (opens in a new tab)
Complex example with nested objects and arrays
Start by defining your data, and a normalized version of your data
export interface VideoRef {
ref: true
id: string
}
export interface VideoData {
id: string
title: string
author: ProfileData
comments: CommentData[]
}
export interface NormalizedVideoData {
id: string
title: string
author: ProfileRef
comments: ProfileRef[]
}
Then create schema with a normalizer, which will convert all normalizable data into normals
export function getVideoSchema(id: string) {
async function normalizer(video: VideoData, more: NormalizerMore) {
const author = await getProfileRef(video.author, more) // Object
const comments = await Promise.all(video.comments.map(it => getCommentRef(it, more))) // Array
return { ...video, author, comments }
}
return getSchema<VideoData | NormalizedVideoData>(
`/api/theytube/video?id=${id}`,
fetchAsJson,
{ normalizer })
}
(Don't forget to put NormalizedVideoData
in the type parameters)
Since XSWR store normalization is recursive, you can (and should) also define a ref factory for your data
Your video will be able to be contained in larger objects, like a allVideos
array, or a videosPerAuthor
mapping
export async function getVideoRef(video: VideoData | VideoRef, more: NormalizerMore) {
if ("ref" in video) return video // already a reference
const schema = getVideoSchema(video.id)
await schema.normalize(video, more)
return { ref: true, id: video.id } as VideoRef
}
You can then create a query
export function useVideo(id: string) {
const query = useSchema(getVideoSchema, [id])
useFetch(query)
return query
}
And finally, use the normalized version of your data for displaying it
export function Video(props: { id: string }) {
const video = useVideo(props.id)
if (!video.data) return null
return <div className="p-4 border border-solid border-gray-500">
<div className="flex justify-center items-center w-full aspect-video border border-solid border-gray-500">
Some video
</div>
<div className="py-4">
<h1 className="text-xl">
{video.data.title}
</h1>
<Profile
id={video.data.author.id} />
</div>
{video.data.comments.map(dataOrRef =>
<Comment
key={dataOrRef.id}
id={dataOrRef.id} />)}
</div>
}
That's it! You can find a full working example in the theytube test (opens in a new tab)