Puck is the open-source visual editor for React, empowering the next generation of page builders and no-code products. Give us a star on GitHub! ⭐️
Puck has been rapidly growing, and it’s been awesome to watch! 🚀 Developers from all backgrounds are pushing the boundaries of what this open-source visual editor can do. But as more people dive into Puck, one question keeps coming up in our Discord community:
“How can I pass data or share state between components in Puck?”
In other words: how do you make one component react to changes in another? For example, you might create a DropZone
component with a search input, so that any lists dropped inside it can read its value:
At first, Puck’s built-in magic might make you think it handles state in a unique way. But here’s the thing: Puck is just React—and so are the components you pass into it. That means that you can rely on any state library or tool you would normally use to manage and share data between components. For this article, I’ll keep it simple and teach you how to solve this problem by using Context.
Before we get started: I’ll assume you’ve got a basic understanding of Puck and how it works. If you’re new here, that’s totally fine—you’re welcome to follow along! But I’d recommend checking out the getting started guide first to get familiar with the basics
Project Setup
To make things easy, I’ve prepared a basic React project on GitHub with Puck pre-installed and ready to go. Clone and install it by running the following commands in your terminal:
git clone https://github.com/FedericoBonel/basic-puck-app
cd ./basic-puck-app
npm install
Already working on an existing project? No problem at all! You can simply install Puck as a dependency with NPM:
npm i @measured/puck --save
And if you’re using frameworks like Next.js or Remix, Puck offers official recipes to make the setup process seamless.
For this tutorial, I’ll assume you’ve cloned the GitHub repo to keep things straightforward. That said, the concepts and steps will apply to any setup—just update the file names as needed to fit your project’s structure.
Configuring Puck
With your project ready to go, the next step is to configure Puck. Open the src/App.jsx
file and swap its contents with the code below. This will set up Puck with a basic config for dragging and dropping two components:
- A
Dashboard
component that greets users and contains aDropZone
for nesting other components - An
ArticleList
component that displays a list of articles inside theDashboard
// App.jsx
import { Puck, DropZone } from "@measured/puck";
import "@measured/puck/puck.css";
// The configs for your draggable components
// Ideally you would pull these out into their own files
const dashboardConfig = {
render: () => {
return (
<div
style={{
minWidth: "fit-content",
maxWidth: "50%",
margin: "auto",
}}
>
<h3>Welcome User!</h3>
<DropZone zone="list" />
<a href="/signup">Sign up now</a>
<p style={{ fontSize: "14px" }}>
Already have an account? <a href="/signin">Sign in here</a>
</p>
</div>
);
},
};
const articleListConfig = {
render: () => {
// These articles could come from an external data source
const articles = ["Puck", "React State", "NextJS"];
return (
<ul>
{articles.map((article, i) => <li key={i}>{article}</li>)}
</ul>
);
},
};
// The Puck configuration object
const config = {
components: {
Dashboard: dashboardConfig,
ArticleList: articleListConfig,
},
};
// Your editor component
const App = () => {
return <Puck config={config} data={{}} />;
};
export default App;
Once you’ve updated the file, start the application in development mode, and navigate to http://localhost:5173 to verify everything is working as expected:
npm run dev
Great! Your basic setup is now complete. Next, let’s dive into adding shared state to your editor.
Adding Context
React Context is the perfect solution for our problem because it offers a simple way to share and manage data across all your components—both inside and outside the editor. It creates a "global state" that you can access whenever needed, making it ideal for scenarios where you need to pull in data from outside Puck—like the selected theme or the logged-in user—or share data between Puck components.
In this guide, I’ll walk you through two common use cases for React Context within Puck:
-
Accessing data stored outside Puck: We’ll begin by setting up a context containing a logged-in user’s data outside of the
<Puck />
component, and then access it from within Puck components. -
Passing data to a nested component: Next, we’ll set up a search query context within the
Dashboard
. This will allow us to capture a user’s search query, store it in the context, and pass it down to theArticleList
component. The goal is to filter the list of articles based on the user’s query, demonstrating how to pass data between a parent and child Puck component.
Step 1: Define the Context outside Puck
Setting up context in Puck follows the same pattern as any React app. You create a Context provider to define and manage your shared state, wrap it around a parent component, and access or update the state wherever it's required in your app.
Start by creating a new Context for the user data. This Context will include both the user object and a function to update the user state.
// context/userContext.jsx
import { createContext, useContext, useState } from "react";
// Create the Context
const UserContext = createContext(null);
// Custom hook to use the User Context
const useUserContext = () => {
const context = useContext(UserContext);
if (!context) {
throw new Error("useUserContext must be used within a UserProvider");
}
return context;
};
Step 2: Create the Context Provider outside Puck
Next, create a UserProvider
component that will wrap your Puck editor. This provider will manage the user
state and make it available to all children.
For the sake of brevity, I’m using a dummy user and the setter function returned by useState.
// context/userContext.jsx
//... existing setup
//eslint-disable-next-line react/prop-types
const UserProvider = ({ children }) => {
// Start the context state with a dummy user
const [user, setUser] = useState({
name: "John Doe",
});
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
};
export { UserProvider, useUserContext };
Step 3: Integrate the Provider with Puck
To integrate the provider with your Puck editor, simply wrap the editor with the UserProvider
. You can put the UserProvider
anywhere above the editor in your component tree (like in your index
file), and it'll work just fine. Once you've done that, all your editor components will have access to the context!
// App.jsx
import { UserProvider, useUserContext } from "./context/UserContext";
//... existing setup
const App = () => {
return (
<UserProvider>
<Puck config={config} data={{}} />
</UserProvider>
);
};
Step 4: Consume the Context in the Puck Components
Now you can access the UserContext
in any of your Puck components. Following our use case example, let’s update the Dashboard
component so that it displays a “welcome back” message for logged-in users and a “generic welcome” message for guests.
// App.jsx
import { useUserContext } from "./context/UserContext";
//... existing setup
const dashboardConfig = {
render: () => {
// Access the user context
const { user, setUser } = useUserContext();
return (
<div
style={{
minWidth: "fit-content",
maxWidth: "50%",
margin: "auto",
}}
>
{/* Read the user's name */}
<h3>Welcome back {user?.name ?? ""}</h3>
<DropZone zone="list" />
{/* Update the user on logout */}
{user && <button onClick={() => setUser(null)}>Sign out 🏃🚪</button>}
</div>
);
},
};
//... the Puck config
If you're using eslint, you may see the linting error ‘React Hook "useUserContext" is called in function "render" that is neither a React function component nor a custom React Hook function.’ This is because ESLint can’t recognise the syntax of the render function as a component.
To fix this you can pull the render function into its own component and pass it (render: WelcomeCard
), or ask ESLint to ignore the line (eslint-disable-next-line react-hooks/rules-of-hooks
).
Now you can go back to the editor, drag and drop the new Dashboard
component, and you'll see that the UserContext
is being accessed to display the “welcome back” message for the dummy logged-in user:
If you were to publish that data and pass it to the Render component using the same config and UserProvider
, the result would look just as it was in the editor, and you could also click on the sign-out button to clear the logged-in user state:
With this done, you now have a state originating outside of Puck, seamlessly passed to and updated by Puck components! ✔️
Step 5: Adding a search query
Now let's move to our second use case: Passing data from a Puck component to another.
To make this work, we’ll add a search query input and an initialQuery
prop to the Dashboard
component. This will allow users to filter the list of articles and give us the option to set an initial query for them to start with.
// App.jsx
import { createContext, useContext, useState, useEffect } from "react";
//.. existing setup
const dashboardConfig = {
// Set the default props for the initial query field
defaultProps: {
initialQuery: "React",
},
// Create a field for setting the inital query
fields: {
initialQuery: {
label: "Initial Search Query",
type: "text",
},
},
// Read the initial query prop
render: ({ initialQuery }) => {
// ... existing dashboard setup
// Create the state for the search query
const [query, setQuery] = useState(initialQuery);
// Update the query's value from the editor's field
useEffect(() => {
setQuery(initialQuery);
}, [initialQuery]);
return (
<div
style={{
minWidth: "fit-content",
maxWidth: "50%",
margin: "auto",
}}
>
<h3>Welcome back {user?.name ?? ""}</h3>
{/* Create the input the user will use to filter the lists */}
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<DropZone zone="list" />
{user && <button onClick={() => setUser(null)}>Sign out 🏃🚪</button>}
</div>
);
},
};
Step 6: Provide context from a Puck component
Next, let’s set up a context in the Dashboard
component to pass down the query state. This will ensure that when the user updates the search input, the new query is stored in the context and shared with the ArticleList
or any other component dropped inside the Dashboard
.
// App.jsx
//... existing setup
// Create the query context that will pass down the query state
const QueryContext = createContext();
const useQueryContext = () => useContext(QueryContext);
const dashboardConfig = {
render: () => {
// ... existing dashboard setup
return (
<QueryContext.Provider value={{ query, setQuery }}>
{/* ... existing dashboard component */}
</QueryContext.Provider>
);
},
};
//... the Puck config
Step 7: Consume the Context from Puck components
Now, we’ll read the context in the components that are dropped inside the context provider. In our case, we’ll consume the context in the ArticleList
component, which the user has nested inside the Dashboard
via the DropZone. This allows the ArticleList
to respond to changes in the search query and update accordingly.
// App.jsx
//... existing setup
const articleListConfig = {
render: () => {
// Access the query state
const { query } = useQueryContext();
// Filter out the articles based on the query
const articles = ["Puck", "React State", "NextJS"].filter(
(article) => article.indexOf(query) !== -1
);
//... existing return statement
},
};
If you now head into the editor, drag a Dashboard
component onto the canvas, drop an ArticleList
inside it, and modify the initialQuery
field, you’ll see the list dynamically filter the articles based on the query. 🎯
You could even expand this setup by having multiple list components with different content reuse the same query context.
🎉 And that’s it! You now have shared state between nested Puck components. 🎉
Pros & Cons of using React Context
✅ Pros:
- Provides a robust solution for sharing state across components, both inside and outside Puck
- Integrates seamlessly with existing React patterns and components
- Can handle complex logic and state
- Zero external dependencies as React Context comes with React
❌ Cons:
- Performance can degrade if you frequently update state at the top of a large component tree, as every subscriber needs to re-render
- When managing multiple context providers, things can become harder to debug
Taking it further
There are a bunch of ways in which you can improve managing shared state in Puck depending on the complexity of your editor:
- Optimize context usage - If you notice performance issues or unnecessary re-renders, consider splitting your context into smaller, more focused contexts. This allows components to subscribe only to the parts of the state they need, minimizing re-renders.
- Incorporate state libraries - If you have multiple shared states and more complex logic, you could move beyond React Context and use your favorite state library. Whether it’s Redux, Zustand, or another library your project is already using, these can simplify managing complex state and improve rendering performance.
- Leverage server-side state - If your application relies heavily on data fetched from a server, consider using a library like TanStack Query or SWR. These libraries manage caching, re-fetching, and synchronization for you, reducing the need for complex shared client-side state.
Your Turn to Build Smarter with Puck 💡
Taking shared state management in Puck to the next level unlocks a world of possibilities for building dynamic, reactive page builders. I’m excited to see the unique and powerful apps you’ll build using these strategies.
So, if this article has inspired you to build something, you have a question about Puck or you want to contribute, here’s how you can get involved:
- 🌟 Star Puck on GitHub to show your support and inspire others to explore.
- 🤝 Join our Discord community to connect, learn, and collaborate.
- 📢 Follow us on X and Bluesky for sneak peeks, updates, and tips.
- 📘 Explore the documentation for advanced techniques to supercharge your builds.
The future of Puck—and no-code innovation—is in your hands. Start building today, and let’s redefine what’s possible together! 🙌
Top comments (1)
Great write up! I get asked this a lot.
Interestingly, I think the new metadata API proposal might introduce some more patterns for this: github.com/measuredco/puck/issues/798