React GridStack page layout library
npm install gridstack-pageA page builder or grid layout builder based on GradStack.js that provides full React control over component layout, loading and saving, and provides callback functions for adding custom components by dragging or clicking.
- Page builder or Layout builder
- Demo project
- Basic Usage
- Create your project
- install gridstack.js
- install gridstack-page
- Prepare API and custom components for loading and saving page.
- Create API functions used to save and load pages as following api.ts.
- Create your custom react components as widget to add layout.
- Add stackpage page builder as following.
https://github.com/aidabo/gridstack-page-builder
Following API, custom components, callback function samples all in demo project.
npm i gridstack or yarn add gridstack
npm i gridstack-page or yarn add gridstack-page
You can just use localStorage of browser or json server , the demo uses json server.
``js
import { PageProps } from "gridstack-page"
const webApiUrl = "/api"
const apiJsonHeaders = {
'Accept': 'application/json',
'Content-Type': 'application/json',
};
export { apiJsonHeaders, webApiUrl}
export const useLayoutStore = () => {
const dataUrl = ${webApiUrl}/pages;
async function getPageList(): Promise
try {
const response = await fetch(${dataUrl},{HTTP error! Status: ${response.status}
method: "GET",
headers: apiJsonHeaders,
});
if (!response.ok) {
throw new Error();
}
const data = (await response.json()) as PageProps[];
return data;
} catch {
//console.error("fetchPageList error:", error);
return false;
}
}
async function getPageById(pageId: string): Promise
try {
const response = await fetch(${dataUrl}/${pageId},{HTTP error! Status: ${response.status}
method: "GET",
headers: apiJsonHeaders,
});
if (!response.ok) {
throw new Error();
}
const data = (await response.json()) as PageProps;
return data;
} catch (error) {
console.error("getPageById error:", error);
return false;
}
}
async function exists(pageId: string): Promise
const result = await getPageById(pageId);
return result != null && result !== false;
}
async function savePage(data: PageProps): Promise
if (!(await exists(data.id))) {
return await insertPage(data);
} else {
return await updatePage(data);
}
}
async function insertPage(data: PageProps): Promise
try {
const response = await fetch(${dataUrl}, {HTTP error! Status: ${response.status}
method: "POST",
headers: apiJsonHeaders,
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error();
}
const result = (await response.json()) as PageProps;
return result;
} catch (error) {
console.error("insertPage error:", error);
return false;
}
}
async function updatePage(data: PageProps): Promise
try {
const response = await fetch(${dataUrl}/${data.id}, {HTTP error! Status: ${response.status}
method: "PUT",
headers: apiJsonHeaders,
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error();
}
const result = (await response.json()) as PageProps;
return result;
} catch (error) {
console.error("updatePage error:", error);
return false;
}
}
async function deletePage(pageId: string): Promise
try {
const response = await fetch(${dataUrl}/${pageId}, {HTTP error! Status: ${response.status}
method: "DELETE",
headers: apiJsonHeaders,
});
if (!response.ok) {
throw new Error();`
}
const result = (await response.json()) as PageProps;
return result;
} catch (error) {
console.error("deletePage error:", error);
return false;
}
}
Following create custom component Image ImageBlurred, ImageCircle, SimpleCard
`js
import { ComponentMap } from "gridstack-page";
import { ComponentProps } from "gridstack-page";
const useComponentProvider = () => {
const getNewComponentMap = (): ComponentMap => {
return {
Image: function Image({ src, title }: { src: string; title?: string }) {
return (
ImageBlurred: ({
src,
content,
date,
author,
alt,
}: {
src: string;
content?: string;
date?: string;
author?: string;
alt?: string;
}) => {
return (
{date}
ImageCircle: ({ src, alt }: { src: string; alt?: string }) => {
return (
SimpleCard: ({
src,
content,
caption,
author,
date,
}: {
src: string;
content: string;
caption?: string;
author?: string;
date?: string;
}) => {
return (
href="#"
className="resize overflow-auto min-w-[200px] min-h-[200px] max-w-full max-h-[90vh] border border-gray-200 rounded-lg shadow-sm bg-white hover:bg-gray-100 dark:border-gray-400 dark:bg-slate-200 dark:hover:bg-slate-400 flex flex-col md:flex-row items-center md:items-stretch"
style={{ resize: "both" }}
>
{/ Image Section /}
{/ Text Section /}
{content}
const getNewComponentProps = (): ComponentProps => {
return {
Image: {
src: "https://dnicugzydez8x.cloudfront.net/60-think-prd/2025/07/image-26.png",
title: "This is Image",
},
ImageBlurred: {
src: "https://images.unsplash.com/photo-1682407186023-12c70a4a35e0?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2832&q=80",
content: "Growth",
date: "20 July 2022",
author: "Sara Lamalo",
alt: "Nature Image",
title: "This is ImageBlurred",
},
ImageCircle: {
src: "https://images.unsplash.com/photo-1682407186023-12c70a4a35e0?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2832&q=80",
alt: "Circle Image",
title: "This is ImageCircle",
},
SimpleCard: {
src: "https://images.unsplash.com/photo-1682407186023-12c70a4a35e0?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2832&q=80",
content: Growth is not just about numbers, it's about the journey. and the people we meet along the way. life is a journey, not a destination. experience is the best teacher.
Growth is not just about numbers, it's about the journey. and the people we meet along the way. life is a journey, not a destination. experience is the best teacher.
Growth is not just about numbers, it's about the journey. and the people we meet along the way. life is a journey, not a destination. experience is the best teacher.,
date: "20 July 2022",
author: "Ai Dabo",
caption: "This think item",
title: "This is SimpleCard",
},
};
};
return { getNewComponentMap, getNewComponentProps };
};
export { useComponentProvider };
`
`js
import { useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
// Here is lib import ----
import { StackPage } from "gridstack-page";
import {
PageProps,
SaveLayoutFn,
LoadLayoutFn,
GoBackListFn,
} from "gridstack-page";
import "gridstack-page/styles"
import { useLayoutStore } from "./api";
import { useComponentProvider } from "./my-components";
export default function StackPageEdit(props: {mode: string}) {
const { pageid } = useParams<{ pageid: string }>();
const [currentPageid, setCurrentPageid] = useState(pageid || "");
const { getPageById, savePage } = useLayoutStore();
const { getNewComponentMap, getNewComponentProps } = useComponentProvider();
const navigate = useNavigate();
// page save callback
const saveLayout: SaveLayoutFn = async (
pageid: string,
pageProps: PageProps
) => {
await savePage(pageProps);
if(pageid !== pageProps.id) {
navigate(/edit/${pageProps.id});
}
};
// page load callback
const loadLayout: LoadLayoutFn = async (
pageid: string
): Promise
setCurrentPageid(pageid);
const page: any = await getPageById(pageid);
if (page === false) {
console.log("new page created: " + pageid);
}
return page;
};
// page list return callback
const gobackList: GoBackListFn = () => {
navigate("/");
};
return (
<>
pageMode={props.mode as any}
onSaveLayout={saveLayout}
onLoadLayout={loadLayout}
gobackList={gobackList}
// can ignore
componentMapProvider={getNewComponentMap}
componentPropsProvider={getNewComponentProps}
/>
>
);
}
``