React
Gumnut provides a set of ready-to-use React components that make it easy to integrate collaborative form editing experiences into your applicaitons.
Its API is much like a "form management library", but it backs onto collaborative state.
Getting Started
At a high-level, you have to perform a few steps to use Gumnut:
Provide a global configuration via
configureGumnut
(or environment variables)Call
useGumnutDoc
with the document ID you'll be using (typically this will match the ID of a database row), along with a method providing a JWT granting access for the current userAdd
<GumnutText>
(for collaborative text) or<GumunutData>
(for managed input fields, e.g., a slider or checkbox) to your components using the result fromuseGumnutDoc
Call
actions.load()
with your initial data (e.g., in a dependency-freeuseEffect
, or a RemixclientLoader
)
- Gumnut is about editing your server data, and you need to provide it
- Every user will provide the same data, which is a no-op
- Later, call
actions.commit()
to enact a change, and you'll be given a callback where you can commit to your database
- If this commit is successful, Gumnut will take a snapshot, recording attributed changes over time
In-Detail
Before you start, you'll need to:
- add "@gumnutdev/react" to your React project using your favorite package manager (we prefer pnpm)
- create a project on the dashboard, including creating a "local dev key" under API Keys
Note also that this library has not been completely tested with SSR approaches to React. Please contact us if you have trouble—we'd love to fix it.
1. Provide Global Configuration
In your project, be sure to call configureGumnut
somewhere, e.g., in your entrypoint:
import { configureGumnut } from "@gumnutdev/react";
configureGumnut({
projectId: "your-project-id-here",
localDevKey: "...", // get this from dashboard during local dev only
});
Alternatively, you can also expose the environment variables GUMNUT_PROJECT_ID
and GUMNUT_LOCAL_DEV_KEY
though your dev environment. This might be safer than specifying this in code, as your localDevKey
gives dangerous levels of access to your project.
2. Call useGumnutDoc
In a component, such as a form, set up a connection to a document of your choice:
import { useGumnutDoc, buildTestToken } from "@gumnutdev/react";
function YourComponent() {
const getToken = () => buildTestToken();
const scope = useGumnutDoc({ getToken, docId: "your-document-id" });
// ...
}
For now, you'll use buildTestToken
to generate a dummy local token with acess to all documents. If you like, this method accepts arguments such as a fixed UID and the user's name/email.
3. Add Compoonents
To use Gumnut, you'll have to add a collaborative component, such as <GumnutText>
. This has a number of options including auto-resize and multiline.
WARNING
Note that this is completely unstyled by default, so the example below includes a border
quite literally so you can see where the component is!
import { useGumnutDoc, buildTestToken, GumnutText } from "@gumnutdev/react";
function YourComponent() {
// ... setup here
return (
<>
<GumnutText
control={scope.control}
name="an-input"
rows={4}
resize="auto"
multiline
placeholder="Some data goes here"
style={{
background: "white",
border: "2px solid #eee",
borderRadius: "4px",
}}
/>
</>
);
}
Unlike regular form elements, you do not provide a value
here—this is automatically bound to the Gumnut state.
Now, you should open your page in several tabs and try typing: you should see your other selves' cursors! Be sure to also open the dashboard and watch the edits on the "Data Index" page.
3a. Troubleshooting
If you have trouble, you can add the <GumnutStatus>
element anywhere in your page which simply emits a loud error if there is a problem. Perhaps your projectId
or other config is incorrect. Otherwise, hit us up on Discord to ask for help!
3b. Data-Only Components
The <GumnutData>
element operates similarly to <GumnutText>
but accepts a render
prop which can be used to control traditional input or other elements. You should probably not use this for text, as you'll only have "last-person-wins" behavior and no cursors.
Use it like this:
import { useGumnutDoc, buildTestToken, GumnutData } from "@gumnutdev/react";
function YourComponent() {
// ... setup here
return (
<>
<GumnutData
control={scope.control}
name="an-input"
render={(arg) => (
<select {...arg.field}>
<option value="">Default</option>
<option value="1">One</option>
<option value="2">Two</option>
</select>
)}
/>
</>
);
}
While out of scope of this brief tutorial, <GumnutData>
can also power focus indicators and the "dirty bit": it does not have to render data or components at all, but rather, it can render metadata for this node, such as who has their cursor here. Be sure to load up its documentation by ctrl-clicking in VSCode or your preferred editor.
4. Call actions.load()
Gumnut is a convenient collaborative editor, but the point of it is to edit your existing data.
Right now, that data is a simple Record<string, string>
of field names to text. Gumnut at launch does not support structured JSON.
The easiest way to ensure that your data is loaded is to call actions.load()
as a result of having the server data available. What this looks like will vary widely based on your stack. One simplistic example might be:
import { useGumnutDoc } from '@gumnutdev/react';
function YourComponent() {
const scope = useGumnutDoc(...);
const data = useFormDataFromServer();
useEffect(() => {
if (data !== undefined) {
scope.actions.load(data);
}
}, [data]);
}
Data that is dirty will not be replaced on additional loads. This is a complex concept; try Gumnut out to get a clearer sense of it.
5. Call actions.commit()
When you're confident you want to make a snapshot of your data, call commit()
. This might be wired up to some button in the form:
function YourComponent() {
const scope = useGumnutDoc(...);
const handleSubmit = async () => {
scope.actions.commit(async ({ changes }) => {
// "changes" contains just the dirty fields
await doLongRunningSaveToYourServer(changes);
});
};
return <>
<button onClick={handleSubmit}>Submit</button>
</>
}
If the callback you pass to commit()
throws an exception, Gumnut will not take a snapshot. If, however, it succeeds, all your fields will be marked "clean" and the snapshot will appear in the dashboard.
You can also call revertAll()
to abandon all changes and reset to your last loaded state (e.g., you might wire this up to a form reset button).
Fin
Phew! That was a very rapid introduction to Gumunt's React components.
Join Discord to ask us questions, or look around the docs for explanations of other core concepts.