...
Cover Image for Subreddit Search

Subreddit Search


Overview

Powered by Nextjs, TypeScript, Tailwindcss, Reddit, Vercel, and Snoowrap via Not an Aardvark. /r/q?=req.query.q. Search Reddit in real-time with rapid data-fetching.

Technical

Wrote custom type defs to transform data fetched on the server side (getStaticProps/getServerSideProps) to a serialized JSON format for client side inference and consumption.

Custom types

export class Serializer<T> {
	serialize(inp: T): string {
		return JSON.stringify(inp);
	}
	deserialize(inp: string): JSONified<T> {
		return JSON.parse(inp);
	}
}

/**
 * @toJSON (): Obj ? JSON.stringify uses Obj returned from toJSON for serialization :
 * JSON.stringify uses the actual properties
 */

export type Widget = {
	toJSON(): {
		kind: 'Widget';
		date: Date;
	};
};

export type Item = {
	text: string;
	count: number;
	// preserve options
	choice: 'yes' | 'no' | null;
	// drop functions
	func: () => void;
	nested: {
		isSaved: boolean;
		data: [1, undefined, 2];
	};
	// pointer to another type
	widget: Widget;
	// Same obj referenced again
	children?: Item[];
};

export type JSONified<T> = JSONifiedValue<
	T extends { toJSON(): infer U } ? U : T
>;

export type JSONifiedValue<T> = T extends
	| string
	| number
	| boolean
	| null
	? T
	: T extends Function
	? never
	: T extends object
	? JSONifiedObject<T>
	: T extends Array<infer U>
	? JSONifiedArray<U>
	: never;

export type JSONifiedObject<T> = {
	[P in keyof T]: JSONifiedObject<T[P]>;
};

export type UndefinedAsNull<T> = T extends undefined ? null : T;

export type JSONifiedArray<T> = Array<
	UndefinedAsNull<JSONified<T>>
>;

// general helpers

export type RecursivePartial<T> = {
	[P in keyof T]?: RecursivePartial<T[P]>;
};

export type RecursiveRequired<T> = {
	[P in keyof T]-?: RecursiveRequired<T[P]>;
};

These types were used as follows

// pages/index.tsx
import { Container } from '../components/UI';
import { Navbar } from '../components/Navbar';
import Head from 'next/head';
import Link from 'next/link';
import cn from 'classnames';
import {
	InferGetStaticPropsType,
	GetStaticPropsContext,
	GetStaticPropsResult
} from 'next';
import { r } from '../lib/snoo-config';
import {
	LandingPage,
	LandingPageWrapper
} from '../components/Landing';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { Serializer } from '../types/json';
import { Meta } from '../components/Meta';
import {
	ChildButtonProps,
	ChildButtonPropsNoUndefinedJSONified
} from '../types/landing';

export function Index({
	childButtonPropsSerialized
}: InferGetStaticPropsType<typeof getStaticProps>) {
	const [search, setSearch] = useState('');
	const { asPath } = useRouter();

	useEffect(() => {
		const pathSubString = asPath.split('/');
		if (!asPath.includes('/r/[display_name]/')) {
			setSearch('');
			return;
		}
		if (
			asPath.includes('/r/[display_name]/') &&
			asPath.length === 3
		) {
			setSearch(pathSubString[3]);
			return;
		}
		// eslint-disable-next-line no-console
		console.log(search);
	}, [asPath]);

	const childButtonPropsDeserialized: ChildButtonProps[] = new Serializer().deserialize(
		childButtonPropsSerialized
	);
	const childButtons = (
		<>
			{childButtonPropsDeserialized
				? childButtonPropsDeserialized.map(prop => {
						return (
							<>
								<Link
									href={'/r/[display_name]'} // = /r/subreddit/query
									as={`/r/${prop.display_name}`}
									key={prop.title}
									passHref={true}
								>
									<a
										type='button'
										key={prop.id}
										className={cn(
											'flex normal-case w-full min-w-full mx-auto sm:w-auto items-center justify-center px-4 py-3 border border-transparent text-lg font-semibold rounded-full shadow-sm text-gray-100 bg-opacity-25 bg-redditSearch ring-2 ring-rojo-100 ring-inset hover:bg-black hover:text-gray-50  sm:px-8 z-50 transition-colors duration-150'
										)}
									>
										/{`${prop.display_name_prefixed}`}
									</a>
								</Link>
							</>
						);
				  })
				: ''}
		</>
	);
	return (
		<>
			<Meta />
			<Navbar />
			<Head>
				<title>{'Subreddit Search Home'}</title>
			</Head>
			<Container clean className='fit'>
				<LandingPageWrapper>
					<LandingPage>{childButtons}</LandingPage>
				</LandingPageWrapper>
			</Container>
		</>
	);
}

export async function getStaticProps(
	ctx: GetStaticPropsContext
): Promise<
	GetStaticPropsResult<{
		childButtonPropsSerialized: string;
	}>
> {
	const snooSubreddit = await r.searchSubreddits({
		query: ctx.params ? (ctx.params.q as string) : 'snowboarding',
		limit: 10,
		count: 10,
		show: '10'
	});

	const snooSubreddtoJSON = snooSubreddit.toJSON();

	const snooSubredd: ChildButtonPropsNoUndefinedJSONified = snooSubreddtoJSON.map(
		snooSub => {
			const {
				display_name_prefixed,
				url,
				id,
				title,
				display_name
			} = snooSub;
			return {
				display_name_prefixed,
				url,
				id,
				title,
				display_name
			};
		}
	);
	const childButtonPropsSerialized = new Serializer().serialize(
		JSON.parse(JSON.stringify(snooSubredd))
	);

	return {
		props: {
			childButtonPropsSerialized
		},
		revalidate: 10
	};
}
export default Index;

Where ChildButtonPropsNoUndefinedJSONified is defined in @/types/landing.ts as follows

export interface ChildButtonPropsNoUndefinedJSONified
	extends Array<UndefinedAsNull<JSONified<ChildButtonProps>>> {}

Serialization/Deserialization to achieve server-to-client prop shuttling visualized

Server-To-Client-Serialization-Deserialization