How to use Zustand with Supabase and Next.js App Router?

Özer
lamalab
Published in
4 min readAug 18, 2024
Lama Lab

Introduction

Recently, I was building a social media app and chose Supabase for the database, Zustand for state management, and Next.js for server-side rendering capabilities. In this case, I want to show you how you can update your database while immediately reflecting changes to your users. Let’s get started!

Sections

  • Creating the Post Interface
  • Creating the Zustand Store for Posts
  • Server Actions for the Home Page
  • Creating a Post for Both Supabase and Zustand
  • Creating Home Page Component
  • Showing Posts to Users

Creating the Post Interface

First, we need to create the Post interface to ensure that the types are correct for our project, states, and database.

//types/Post.ts
interface Post {
id: number;
content: string;
userId: number;
created_at: string;
avatarUrl: string;
userName: string;
name: string;
}

Creating the Zustand Store for Posts

We have created our interface. Now, with the Post interface, we can create a Zustand store to use our posts in different components.

//store/index.ts
import { create } from 'zustand';

import { createClient } from '@/utils/supabase/client';

const supabase = createClient();

interface PostStore {
posts: Post[];
fetchPosts: () => Promise<void>;
addPost: (post: Post) => void;
}

export const usePostStore = create<PostStore>(set => ({
posts: [],
fetchPosts: async () => {
const { data, error } = await supabase.from('posts').select('*');
if (error) {
console.error('Error fetching posts:', error);
} else {
set({ posts: data });
}
},
addPost: post => set(state => ({ posts: [...state.posts, post] })),
}));

const postStore = usePostStore.getState();
postStore.fetchPosts();

As you can see, we also have a PostStore interface to define usePostStore. With usePostStore, we can create, set, and use posts in any component in our project. Additionally, we initially fetch posts from Supabase to dynamically link our store to the database.

Server Actions for the Home Page

We need to interact with server actions within our form to insert a post into our Supabase table. Let’s define our actions!

//modules/HomePage/actions.ts
export async function createPost(formData: FormData): Promise<Post[]> {
const supabase = createClient();

const content = formData.get('content') as string;

if (!content) {
throw new Error('Content is required');
}

const { data, error } = await supabase
.from('posts')
.insert({
content,
userId: user.id,
avatarUrl: user.user_metadata.avatar_url,
userName: user.user_metadata.user_name,
name: user.user_metadata.full_name,
tags,
})
.select()
.returns<Post[]>();

if (error) {
throw new Error(error.message);
}

return data;
}

The important part of this code is that we are returning a Promise<Post[]>. After we insert a post, we select the post, and Supabase returns the inserted post within a single-element array. We will see how to use actions with Zustand in the next section.

Creating a Post for Both Supabase and Zustand

We have defined our action; now we need to actually create a post for both our store and the database. Note that I’m using a Drawer component in my real codebase, so don't worry about the naming here—it's just a component that allows us to create a post. Let's inspect the code!

//modules/HomePage/CreatePostDrawer.tsx
'use client';

import { usePostStore } from '@/store';

import { createPost } from './actions';

export default function CreatePostDrawer() {
const addPost = usePostStore(state => state.addPost);

return (
//... existing content
<form
action={async e => {
const newPost = await createPost(e, tagsList);
if (newPost) {
addPost(newPost[0]);
}
}}
>
<Textarea
name="content"
placeholder="Type your message here."
/>
</form>
//... existing content
);
}

Here, we are retrieving addPost from our store. Inside the form action, we first update our database. After that, as I mentioned, it returns a promise, and with that promise, we add a new post to our store. Note that Supabase returns a single-element array, so we need to access the new post with newPost[0].

Creating Home Page Component

//modules/HomePage/index.tsx
import { PostgrestSingleResponse } from '@supabase/supabase-js';

import { createClient } from '@/utils/supabase/server';

import CreatePostDrawer from './CreatePostDrawer';
import Posts from './Posts';

export default async function HomePage() {
return (
<main>
<Posts/>
<CreatePostDrawer />
</main>
);
}

Here, we don’t need to fetch posts from Supabase directly; we’re essentially handling the initial render as mentioned. Now, let’s see the fun part: how we show our posts to users!

Showing Posts to Users

In the component, you will see the Image, Link components from Next.js, and dayjs. For now, let's not focus on them. We need to focus on how we are using Zustand to update posts immediately and how we put posts into the Zustand store on the first render.

//modules/HomePage/Posts.tsx
'use client';

import { useEffect } from 'react';

import Image from 'next/image';
import Link from 'next/link';

import { PostgrestSingleResponse } from '@supabase/supabase-js';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);

import { usePostStore } from '@/store';

interface PostsProps {
posts: PostgrestSingleResponse<Post[]>;
}

export default function Posts({ posts }: PostsProps) {
const postsFromStore = usePostStore(state => state.posts);

return (
<>
{postsFromStore.map(post => (
<div
key={post.id}
>
<div>
<Link href={`/profile/${post.userName}`}>
<Image
src={post.avatarUrl}
alt="avatar"
width={60}
height={60}
/>
</Link>
<div>
<div>
<Link
href={`/profile/${post.userName}`}
>
{post.name}
</Link>
<div>@{post.userName}</div>
<div> {dayjs(post.created_at).fromNow()} </div>
</div>
<div>{post.content}</div>
</div>
</div>
</div>
))}
</>
);
}

Here, actually, the component explains itself very briefly after our iterations. On the first render, if posts exist from Supabase, we update our store, and we use the posts from the store in the component. That’s all. We have seen how we can use server actions, Zustand, and Supabase together.

Summary

I think for fresh starters, it’s a good and simple real-life example of how to use Next.js App Router, Zustand, and Supabase together for creating and showing posts to users. I am open to any feedback to improve this approach. Feel free to comment! Thanks for reading.

--

--