3. Detail & Editing

Add show, edit, and create views with optimistic updates.

Adding A Detail View

So far, the admin only has list pages. Additionally, the user list doesn't render all columns, so you need to add a detail view to see all the user fields. The <Resource> component accepts a show component prop to define a detail view. Let's use the <ShowGuesser> to help bootstrap it:

// App.tsx
import { Resource } from "ra-core";
-import { Admin } from "@/components/admin";
+import { Admin, ShowGuesser } from "@/components/admin";
import { dataProvider } from "./data-provider";
import { PostList } from "./posts";
import { UserList } from "./users";
 
const App = () => (
    <Admin dataProvider={dataProvider}>
        <Resource name="posts" list={PostList} />
-        <Resource name="users" list={UserList} />
+        <Resource name="users" list={UserList} show={ShowGuesser} />
    </Admin>
);
 
export default App;

Now you can click on a user in the list to see their details:

Just like for other guessed components, you can customize the show view by copying the code logged to the console by the <ShowGuesser> and modifying it to suit your needs.

Now that the users resource has a show view, the ReferenceField in the userId column of the posts list links to it by default. Reference components let users navigate from one resource to another naturally. They are a key feature of shadmin.

Adding Editing Capabilities

An admin interface isn't just about displaying remote data; it should also allow editing records. shadmin provides an <Edit> component for this purpose. Let's use the <EditGuesser> to help bootstrap it.

// App.tsx
import { Resource } from "ra-core";
-import { Admin, ShowGuesser } from "@/components/admin";
+import { Admin, EditGuesser, ShowGuesser } from "@/components/admin";
import { dataProvider } from "./data-provider";
import { PostList } from "./posts";
import { UserList } from "./users";
 
const App = () => (
    <Admin dataProvider={dataProvider}>
-        <Resource name="posts" list={PostList} />
+        <Resource name="posts" list={PostList} edit={EditGuesser} />
        <Resource name="users" list={UserList} show={ShowGuesser} />
    </Admin>
);

Users can display the edit page by clicking on the Edit button in the posts list.

The form is already functional. It issues PUT requests to the REST API upon submission. The userId is editable in an autocomplete input, and Shadmin automatically fetches the possible values.

Copy the <PostEdit> code dumped by the guesser in the console to the posts.tsx file so that you can customize the view:

// posts.tsx
import {
    DataTable,
    List,
    ReferenceField,
    AutocompleteInput,
    Edit,
    ReferenceInput,
    SimpleForm,
    TextInput,
    EditButton,
} from "@/components/admin";
 
export const PostList = () => (
    /* ... */
);
 
export const PostEdit = () => (
    <Edit>
        <SimpleForm>
            <ReferenceInput source="userId" reference="users">
                <AutocompleteInput />
            </ReferenceInput>
            <TextInput source="id" />
            <TextInput source="title" />
            <TextInput source="body" />
        </SimpleForm>
    </Edit>
);

Use that component as the edit prop of the "posts" resource instead of the guesser:

// App.tsx
import { Resource } from "ra-core";
-import { Admin, EditGuesser, ShowGuesser } from "@/components/admin";
+import { Admin, ShowGuesser } from "@/components/admin";
import { dataProvider } from "./data-provider";
-import { PostList } from "./posts";
+import { PostEdit, PostList } from "./posts";
import { UserList } from "./users";
 
const App = () => (
    <Admin dataProvider={dataProvider}>
-        <Resource name="posts" list={PostList} edit={EditGuesser} />
+        <Resource name="posts" list={PostList} edit={PostEdit} />
        <Resource name="users" list={UserList} show={ShowGuesser} />
    </Admin>
);

You can now adjust the <PostEdit> component to disable editing of the primary key (id), place it first, as follows:

// posts.tsx
export const PostEdit = () => (
    <Edit>
        <SimpleForm>
+           <TextInput source="id" disabled />
            <ReferenceInput source="userId" reference="users">
                <AutocompleteInput />
            </ReferenceInput>
-            <TextInput source="id" />
            <TextInput source="title" />
            <TextInput source="body" />
        </SimpleForm>
    </Edit>
);

If you've understood the <List> component, the <Edit> component will be no surprise. It's responsible for fetching the record and displaying the page title. It passes the record down to the <SimpleForm> component, which is responsible for the form layout, default values, and validation. Just like <DataTable>, <SimpleForm> uses its children to determine the form inputs to display. It expects input components as children. <TextInput> and <ReferenceInput> are such inputs.

The <ReferenceInput> takes the same props as the <ReferenceField> (used earlier in the <PostList> page). <ReferenceInput> uses these props to fetch the API for possible references related to the current record (in this case, possible users for the current post). It then creates a context with the possible choices and renders an <AutocompleteInput>, which is responsible for displaying the choices and letting the user select one.

Adding Creation Capabilities

Let's allow users to create posts, too. Copy the <PostEdit> component into a <PostCreate>, and replace <Edit> with <Create>:

// posts.tsx
import {
+    Create,
    DataTable,
    List,
    ReferenceField,
    AutocompleteInput,
    Edit,
    ReferenceInput,
    SimpleForm,
    TextInput,
    EditButton,
} from "@/components/admin";
 
export const PostList = () => (
    /* ... */
);
 
export const PostEdit = () => (
    /* ... */
);
 
+export const PostCreate = () => (
+   <Create>
+       <SimpleForm>
+           <ReferenceInput source="userId" reference="users">
+               <AutocompleteInput />
+           </ReferenceInput>
+           <TextInput source="title" />
+           <TextInput source="body" />
+       </SimpleForm>
+   </Create>
+);
 

Tip: The <PostEdit> and the <PostCreate> components use almost the same child form, except for the additional id input in <PostEdit>. In most cases, the forms for creating and editing a record are a bit different, because most APIs create primary keys server-side. But if the forms are the same, you can share a common form component in <PostEdit> and <PostCreate>.

To use the new <PostCreate> component in the posts resource, just add it as the create attribute in the <Resource name="posts"> component:

// App.tsx
import { Resource } from "ra-core";
import { Admin, ShowGuesser } from "@/components/admin";
import { dataProvider } from "./data-provider";
-import { PostEdit, PostList } from "./posts";
+import { PostCreate, PostEdit, PostList } from "./posts";
import { UserList } from "./users";
 
const App = () => (
    <Admin dataProvider={dataProvider}>
-        <Resource name="posts" list={PostList} edit={PostEdit} />
+        <Resource
+            name="posts"
+            list={PostList}
+            edit={PostEdit}
+            create={PostCreate}
+        />
        <Resource name="users" list={UserList} show={ShowGuesser} />
    </Admin>
);

shadmin automatically adds a "create" button on top of the posts list to give access to the post create page. And the creation form works; it issues a POST request to the REST API upon submission.

Optimistic Rendering And Undo

Unfortunately, JSONPlaceholder is a read-only API; although it seems to accept POST and PUT requests, it doesn't take into account the creations and edits - that's why, in this particular case, you will see errors after creation, and you won't see your edits after you save them. It's just an artifact of JSONPlaceholder.

But then, how come the newly created post appears in the list just after creation in the screencast above?

That's because shadmin uses optimistic updates. When a user edits a record and hits the "Save" button, the UI shows a confirmation and displays the updated data before sending the update query to the server. The main benefit is that UI changes are immediate—no need to wait for the server response. It's a great comfort for users.

But there is an additional benefit: it also allows the "Undo" feature. Undo is already functional in the admin at this point. Try editing a record, then hit the "Undo" link in the black confirmation box before it slides out. You'll see that the app does not send the UPDATE query to the API and displays the non-modified data.

Even though updates appear immediately due to optimistic rendering, shadmin only sends them to the server after a short delay (about 5 seconds). During this delay, the user can undo the action, and shadmin will never send the update.

Optimistic updates and undo require no specific code on the API side—shadmin handles them purely on the client side. That means you'll get them for free with your own API!