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) { 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 getSingleSchema<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 getSingleSchema<(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 useQuery(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 = useQuery(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
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 getSingleSchema<VideoData | NormalizedVideoData, Error>( `/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 = useQuery(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