ra-realtime
Teams where several people work in parallel on a common task need to allow real-time notifications, and prevent data loss when two editors work on the same resource concurrently. ra-realtime
provides hooks and UI components to lock records, live update views when a change occurs in the background, and notify end users of these events.
Test it live in the Enterprise Edition Storybook and in the e-commerce demo.
Installation
npm install --save @react-admin/ra-realtime
# or
yarn add @react-admin/ra-realtime
Tip: ra-realtime
is part of the React-Admin Enterprise Edition, and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package.
The package contains new translation messages (in English and French). You should add them to your i18nProvider
. For instance, to add English messages:
import { Admin } from 'react-admin';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
import frenchMessages from 'ra-language-french';
import {
raRealTimeLanguageEnglish,
raRealTimeLanguageFrench,
} from '@react-admin/ra-realtime';
const messages = {
en: { ...englishMessages, ...raRealTimeLanguageEnglish },
fr: { ...frenchMessages, ...raRealTimeLanguageFrench },
};
const i18nProvider = polyglotI18nProvider(locale => messages[locale], 'en');
const App = () => <Admin i18nProvider={i18nProvider}>{/* ... */}</Admin>;
import { Admin } from "react-admin";
import polyglotI18nProvider from "ra-i18n-polyglot";
import englishMessages from "ra-language-english";
import frenchMessages from "ra-language-french";
import { raRealTimeLanguageEnglish, raRealTimeLanguageFrench } from "@react-admin/ra-realtime";
const messages = {
en: { ...englishMessages, ...raRealTimeLanguageEnglish },
fr: { ...frenchMessages, ...raRealTimeLanguageFrench },
};
const i18nProvider = polyglotI18nProvider((locale) => messages[locale], "en");
const App = () => <Admin i18nProvider={i18nProvider}>{/* ... */}</Admin>;
Live Updates
Ra-realtime provides live updates via alternative components to <List>
, <Edit>
, and <Show>
. Just use the components from @react-admin/ra-realtime
instead of their react-admin
counterpart.
For instance, replace <List>
with <RealTimeList>
to have a list refreshing automatically when an element is added, updated, or deleted:
import {
- List,
ListProps,
Datagrid,
TextField,
NumberField,
Datefield,
} from 'react-admin';
+import { RealTimeList } from '@react-admin/ra-realtime';
const PostList = (props: ListProps) => (
- <List {...props}>
+ <RealTimeList {...props}>
<Datagrid>
<TextField source="title" />
<NumberField source="views" />
<DateField source="published_at" />
</Datagrid>
- </List>
+ </RealTimeList>
);
Check the <RealTimeList>
, <RealTimeEdit>
, and <RealTimeShow>
components for details.
Menu Badges
Ra-realtime also provides badge notifications in the Menu, so that users can see that something new happened to a resource list while working on another one.
Use <RealTimeMenu>
instead of react-admin's <Menu>
to get this feature:
import React from 'react';
import { Admin, Layout, LayoutProps, Resource } from 'react-admin';
import { RealTimeMenu } from '@react-admin/ra-realtime';
import { PostList, PostShow, PostEdit, realTimeDataProvider } from '.';
const CustomLayout = (props: LayoutProps) => (
<Layout {...props} menu={RealTimeMenu} />
);
const MyReactAdmin = () => (
<Admin dataProvider={realTimeDataProvider} layout={CustomLayout}>
<Resource
name="posts"
list={PostList}
show={PostShow}
edit={PostEdit}
/>
</Admin>
);
import React from "react";
import { Admin, Layout, Resource } from "react-admin";
import { RealTimeMenu } from "@react-admin/ra-realtime";
import { PostList, PostShow, PostEdit, realTimeDataProvider } from ".";
const CustomLayout = (props) => <Layout {...props} menu={RealTimeMenu} />;
const MyReactAdmin = () => (
<Admin dataProvider={realTimeDataProvider} layout={CustomLayout}>
<Resource name="posts" list={PostList} show={PostShow} edit={PostEdit} />
</Admin>
);
Locks
And last but not least, ra-realtime provides a lock mechanism to prevent editing or deleting the same record as another user.
This feature leverages specialized hooks, like useLockRecord
:
import {
useLockRecord,
useGetLock,
useGetRecordId,
} from '@react-admin/ra-realtime';
import {
Edit,
SimpleForm,
TextInput,
useResourceContext,
Toolbar,
SaveButton,
} from 'react-admin';
import { CircularProgress } from '@mui/material';
import React from 'react';
const MyLockedEditView = () => {
const { isLoading } = useLockRecord();
if (isLoading) {
return <CircularProgress />;
}
return (
<Edit>
<SimpleForm toolbar={<CustomToolbar />}>
<TextInput source="title" />
</SimpleForm>
</Edit>
);
};
const CustomToolbar = () => {
const resource = useResourceContext();
const id = useGetRecordId();
const { data } = useGetLock(resource, { id });
const isMarioLocker = data?.identity === 'mario';
// Prevent clicking on the `<SaveButton>` if someone else is locking this record
return (
<Toolbar>
<SaveButton disabled={!isMarioLocker} />
</Toolbar>
);
};
import { useLockRecord, useGetLock, useGetRecordId } from "@react-admin/ra-realtime";
import { Edit, SimpleForm, TextInput, useResourceContext, Toolbar, SaveButton } from "react-admin";
import { CircularProgress } from "@mui/material";
import React from "react";
const MyLockedEditView = () => {
const { isLoading } = useLockRecord();
if (isLoading) {
return <CircularProgress />;
}
return (
<Edit>
<SimpleForm toolbar={<CustomToolbar />}>
<TextInput source="title" />
</SimpleForm>
</Edit>
);
};
const CustomToolbar = () => {
const resource = useResourceContext();
const id = useGetRecordId();
const { data } = useGetLock(resource, { id });
const isMarioLocker = data?.identity === "mario";
// Prevent clicking on the `<SaveButton>` if someone else is locking this record
return (
<Toolbar>
<SaveButton disabled={!isMarioLocker} />
</Toolbar>
);
};
DataProvider For Real-Time Events
To enable real-time features, the dataProvider
must implement three new methods:
subscribe(topic, callback)
unsubscribe(topic, callback)
publish(topic, event)
These methods should return a Promise resolved when the action was acknowledged by the real-time bus.
In addition, to support the lock features, the dataProvider
must implement 4 more methods:
lock(resource, { id, identity, meta })
unlock(resource, { id, identity, meta })
getLock(resource, { id, meta })
getLocks(resource, { meta })
API-Platform Adapter
The ra-realtime
package contains a function augmenting a regular (API-based) dataProvider
with real-time methods based on the capabilities of API-Platform. Use it as follows:
import { Datagrid, EditButton, ListProps } from 'react-admin';
import {
HydraAdmin,
ResourceGuesser,
FieldGuesser,
hydraDataProvider,
} from '@api-platform/admin';
import {
RealTimeList,
addRealTimeMethodsBasedOnApiPlatform,
} from '@react-admin/ra-realtime';
const dataProvider = hydraDataProvider('https://localhost:8443');
const realTimeDataProvider = addRealTimeMethodsBasedOnApiPlatform(
// The original dataProvider (should be an hydra data provider passed by API-Platform)
dataProvider,
// The API-Platform Mercure Hub URL
'https://localhost:1337/.well-known/mercure',
// JWT token to authenticate against the API-Platform Mercure Hub
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.obDjwCgqtPuIvwBlTxUEmibbBf0zypKCNzNKP7Op2UM',
// The topic URL used by API-Platform (without slash at the end)
'https://localhost:8443'
);
const App = () => {
return (
<HydraAdmin
entrypoint="https://localhost:8443"
dataProvider={realTimeDataProvider}
>
<ResourceGuesser name="greetings" list={GreetingsList} />
</HydraAdmin>
);
};
// Example for connecting a list of greetings
const GreetingsList = () => (
<RealTimeList>
<Datagrid>
<FieldGuesser source="name" />
<EditButton />
</Datagrid>
</RealTimeList>
);
import { Datagrid, EditButton } from "react-admin";
import { HydraAdmin, ResourceGuesser, FieldGuesser, hydraDataProvider } from "@api-platform/admin";
import { RealTimeList, addRealTimeMethodsBasedOnApiPlatform } from "@react-admin/ra-realtime";
const dataProvider = hydraDataProvider("https://localhost:8443");
const realTimeDataProvider = addRealTimeMethodsBasedOnApiPlatform(
// The original dataProvider (should be an hydra data provider passed by API-Platform)
dataProvider,
// The API-Platform Mercure Hub URL
"https://localhost:1337/.well-known/mercure",
// JWT token to authenticate against the API-Platform Mercure Hub
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.obDjwCgqtPuIvwBlTxUEmibbBf0zypKCNzNKP7Op2UM",
// The topic URL used by API-Platform (without slash at the end)
"https://localhost:8443"
);
const App = () => {
return (
<HydraAdmin entrypoint="https://localhost:8443" dataProvider={realTimeDataProvider}>
<ResourceGuesser name="greetings" list={GreetingsList} />
</HydraAdmin>
);
};
// Example for connecting a list of greetings
const GreetingsList = () => (
<RealTimeList>
<Datagrid>
<FieldGuesser source="name" />
<EditButton />
</Datagrid>
</RealTimeList>
);
Mercure Adapter
The ra-realtime
package contains a function augmenting a regular (API-based) dataProvider
with real-time methods based on a Mercure hub. Use it as follows:
import { addRealTimeMethodsBasedOnMercure } from '@react-admin/ra-realtime';
const realTimeDataProvider = addRealTimeMethodsBasedOnMercure(
// original dataProvider
dataProvider,
// Mercure hub url
'http://path.to.my.api/.well-known/mercure',
// JWT token to authenticate against the Mercure Hub
'eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiKiJdfX0.SWKHNF9wneXTSjBg81YN5iH8Xb2iTf_JwhfUY5Iyhsw'
);
const App = () => (
<Admin dataProvider={realTimeDataProvider}>{/* ... */}</Admin>
);
import { addRealTimeMethodsBasedOnMercure } from "@react-admin/ra-realtime";
const realTimeDataProvider = addRealTimeMethodsBasedOnMercure(
// original dataProvider
dataProvider,
// Mercure hub url
"http://path.to.my.api/.well-known/mercure",
// JWT token to authenticate against the Mercure Hub
"eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiKiJdfX0.SWKHNF9wneXTSjBg81YN5iH8Xb2iTf_JwhfUY5Iyhsw"
);
const App = () => <Admin dataProvider={realTimeDataProvider}>{/* ... */}</Admin>;
Writing a Custom Adapter
If you're using another transport for real-time messages (WebSockets, long polling, GraphQL subscriptions, etc.), you'll have to implement subscribe
, unsubscribe
, and publish
yourself in your dataProvider
. As an example, here is an implementation using a local variable, that ra-realtime
uses in tests:
let subscriptions = [];
const dataProvider = {
// regular dataProvider methods like getList, getOne, etc,
// ...
subscribe: async (topic, subscriptionCallback) => {
subscriptions.push({ topic, subscriptionCallback });
return Promise.resolve({ data: null });
},
unsubscribe: async (topic, subscriptionCallback) => {
subscriptions = subscriptions.filter(
subscription =>
subscription.topic !== topic ||
subscription.subscriptionCallback !== subscriptionCallback
);
return Promise.resolve({ data: null });
},
publish: (topic, event) => {
if (!topic) {
return Promise.reject(new Error('ra-realtime.error.topic'));
}
if (!event.type) {
return Promise.reject(new Error('ra-realtime.error.type'));
}
subscriptions.map(
subscription =>
topic === subscription.topic &&
subscription.subscriptionCallback(event)
);
return Promise.resolve({ data: null });
},
};
let subscriptions = [];
const dataProvider = {
// regular dataProvider methods like getList, getOne, etc,
// ...
subscribe: async (topic, subscriptionCallback) => {
subscriptions.push({ topic, subscriptionCallback });
return Promise.resolve({ data: null });
},
unsubscribe: async (topic, subscriptionCallback) => {
subscriptions = subscriptions.filter(
(subscription) => subscription.topic !== topic || subscription.subscriptionCallback !== subscriptionCallback
);
return Promise.resolve({ data: null });
},
publish: (topic, event) => {
if (!topic) {
return Promise.reject(new Error("ra-realtime.error.topic"));
}
if (!event.type) {
return Promise.reject(new Error("ra-realtime.error.type"));
}
subscriptions.map((subscription) => topic === subscription.topic && subscription.subscriptionCallback(event));
return Promise.resolve({ data: null });
},
};
You can check the behavior of the real-time components by using the default console logging provided in addRealTimeMethodsInLocalBrowser
.
Lock Support
The 4 data provider lock methods (lock
, unlock
, getLock
, and getLocks
) accept the following parameters:
resource
: the resource name (eg.'posts'
)id
: the record id (eg.123
)identity
: an identifier (string or number) corresponding to the identity of the locker (eg.'julien'
). This could be an authentication token for instance.meta
: an object that will be forwarded to the dataProvider (optional)
The ra-realtime
package offers a function augmenting a regular (API-based) dataProvider
with locks methods based on a locks
resource.
GET /locks?sort=["id","ASC"]&range=[0, 1]&filter={"resource":"people","recordId":"18"}
POST /locks
The POST
query should contain the following body:
{
"identity": "Toad",
"resource": "people",
"recordId": 18,
"createdAt": "2020-09-29 10:20"
}
Please note that the identity
and the createdAt
formats depend on your API.
Here is how to use it in your react-admin application:
import { Admin } from 'react-admin';
import { addLocksMethodsBasedOnALockResource } from '@react-admin/ra-realtime';
const dataProviderWithLocks = addLocksMethodsBasedOnALockResource(
dataProvider // original dataProvider
);
const App = () => (
<Admin dataProvider={dataProviderWithLocks}>{/* ... */}</Admin>
);
import { Admin } from "react-admin";
import { addLocksMethodsBasedOnALockResource } from "@react-admin/ra-realtime";
const dataProviderWithLocks = addLocksMethodsBasedOnALockResource(
dataProvider // original dataProvider
);
const App = () => <Admin dataProvider={dataProviderWithLocks}>{/* ... */}</Admin>;
Topic And Event Format
You've noticed that all the dataProvider
real-time methods expect a topic
as the first argument. A topic
is just a string, identifying a particular real-time channel. Topics can be used e.g. to dispatch messages to different rooms in a chat application or to identify changes related to a particular record.
Most ra-realtime
components deal with CRUD logic, so ra-realtime
subscribes to special topics named resource/[name]
and resource/[name]/[id]
. For your own events, use any topic
you want.
Publishers send an event
to all the subscribers. An event
should be a JavaScript object with a type
and a payload
field. In addition, ra-realtime
requires that every event
contains a topic
- the name of the topic
it's published to.
Here is an example event:
{
topic: 'messages',
type: 'created',
payload: 'New message',
date: new Date(),
}
For CRUD operations, ra-realtime
expects events to use the types 'created', 'updated', and 'deleted'.
CRUD Events
Ra-realtime has deep integration with react-admin, where most of the logic concerns resources and records. To enable this integration, your real-time backend should publish the following events:
- when a new record is created:
{
topic: `resource/${resource}`,
type: 'created',
payload: { ids: [id]},
date: new Date(),
}
- when a record is updated:
{
topic: `resource/${resource}/id`,
type: 'updated',
payload: { ids: [id]},
date: new Date(),
}
{
topic: `resource/${resource}`,
type: 'updated',
payload: { ids: [id]},
date: new Date(),
}
- when a record is deleted:
{
topic: `resource/${resource}/id`,
type: 'deleted',
payload: { ids: [id]},
date: new Date(),
}
{
topic: `resource/${resource}`,
type: 'deleted',
payload: { ids: [id]},
date: new Date(),
}
dataProvider
Methods Directly
Calling the Once you've set a real-time dataProvider
in your <Admin>
, you can call the real-time methods in your React components via the useDataProvider
hook.
For instance, here is a component displaying messages posted to the 'messages' topic in real-time:
import React, { useState } from 'react';
import { useDataProvider, useNotify } from 'react-admin';
const MessageList = () => {
const notify = useNotify();
const [messages, setMessages] = useState([]);
const dataProvider = useDataProvider();
useEffect(() => {
const callback = event => {
// event is like
// {
// topic: 'messages',
// type: 'created',
// payload: 'New message',
// }
setMessages(messages => [...messages, event.payload]);
notify('New message');
};
// subscribe to the 'messages' topic on mount
dataProvider.subscribe('messages', callback);
// unsubscribe on unmount
return () => dataProvider.unsubscribe('messages', callback);
}, [setMessages, notify, dataProvider]);
return (
<ul>
{messages.map((message, index) => (
<li key={index}>{message}</li>
))}
</ul>
);
};
import React, { useState } from "react";
import { useDataProvider, useNotify } from "react-admin";
const MessageList = () => {
const notify = useNotify();
const [messages, setMessages] = useState([]);
const dataProvider = useDataProvider();
useEffect(() => {
const callback = (event) => {
// event is like
// {
// topic: 'messages',
// type: 'created',
// payload: 'New message',
// }
setMessages((messages) => [...messages, event.payload]);
notify("New message");
};
// subscribe to the 'messages' topic on mount
dataProvider.subscribe("messages", callback);
// unsubscribe on unmount
return () => dataProvider.unsubscribe("messages", callback);
}, [setMessages, notify, dataProvider]);
return (
<ul>
{messages.map((message, index) => (
<li key={index}>{message}</li>
))}
</ul>
);
};
And here is a button publishing an event to the messages
topic. All the subscribers to this topic will execute their callback:
import React from 'react';
import { useDataProvider, useNotify } from 'react-admin';
const SendMessageButton = () => {
const dataProvider = useDataProvider();
const notify = useNotify();
const handleClick = () => {
dataProvider
.publish('messages', {
type: 'created',
topic: 'messages',
payload: 'New message',
})
.then(() => {
notify('Message sent');
});
};
return <Button onClick={handleClick}>Send new message</Button>;
};
import React from "react";
import { useDataProvider, useNotify } from "react-admin";
const SendMessageButton = () => {
const dataProvider = useDataProvider();
const notify = useNotify();
const handleClick = () => {
dataProvider
.publish("messages", {
type: "created",
topic: "messages",
payload: "New message",
})
.then(() => {
notify("Message sent");
});
};
return <Button onClick={handleClick}>Send new message</Button>;
};
Tip: You should not need to call dataProvider.publish()
directly very often. Most real-time backends publish events in reaction to a change in the data. So the previous example is fictive. In reality, a typical <SendMessageButton>
would simply call dataProvider.create('messages')
, and the API would create the new message AND publish the 'created' event to the real-time bus.
useSubscribe
In practice, every component that should react to an event needs a useEffect
calling dataProvider.subscribe()
, and returning a callback calling dataProvider.unsubscribe()
. That's why ra-realtime
exposes a useSubscribe
hook, which simplifies that logic a great deal.
Here is the same <MessageList>
as above, but using useSubscribe
:
import React, { useState } from 'react';
import { useNotify } from 'react-admin';
import { useSubscribe } from '@react-admin/ra-realtime';
const MessageList = () => {
const notify = useNotify();
const [messages, setMessages] = useState([]);
useSubscribe('messages', event => {
setMessages([...messages, event.payload]);
notify('New message');
});
return (
<ul>
{messages.map((message, index) => (
<li key={index}>{message}</li>
))}
</ul>
);
};
import React, { useState } from "react";
import { useNotify } from "react-admin";
import { useSubscribe } from "@react-admin/ra-realtime";
const MessageList = () => {
const notify = useNotify();
const [messages, setMessages] = useState([]);
useSubscribe("messages", (event) => {
setMessages([...messages, event.payload]);
notify("New message");
});
return (
<ul>
{messages.map((message, index) => (
<li key={index}>{message}</li>
))}
</ul>
);
};
useSubscribeToRecord
And useSubscribeToRecordList
Ra-realtime provides specialized versions of useSubscribe
, to subscribe to events concerning:
- a single record:
useSubscribeToRecord(callback, resource, id)
- a list of records:
useSubscribeToRecordList(callback, resource)
Thanks to react-admin v4 providing resource and record contexts, the resource
and id
params can be omitted (if we are in such contexts), hence simplifying the code to:
useSubscribeToRecord(callback)
useSubscribeToRecordList(callback)
Using these hooks, you can add real-time capabilities to a <Show>
view for instance:
import { Show, useNotify, useRefresh } from 'react-admin';
import { useSubscribeToRecord } from '@react-admin/ra-realtime';
const PostShow = () => {
const notify = useNotify();
const refresh = useRefresh();
useSubscribeToRecord(event => {
switch (event.type) {
case 'updated': {
refresh();
notify('Record updated server-side');
break;
}
case 'deleted': {
notify('Record deleted server-side', 'warning');
break;
}
default: {
console.log('Unsupported event type', event);
}
}
});
return <Show>{/* ... */}</Show>;
};
import { Show, useNotify, useRefresh } from "react-admin";
import { useSubscribeToRecord } from "@react-admin/ra-realtime";
const PostShow = () => {
const notify = useNotify();
const refresh = useRefresh();
useSubscribeToRecord((event) => {
switch (event.type) {
case "updated": {
refresh();
notify("Record updated server-side");
break;
}
case "deleted": {
notify("Record deleted server-side", "warning");
break;
}
default: {
console.log("Unsupported event type", event);
}
}
});
return <Show>{/* ... */}</Show>;
};
<RealTimeList>
<RealTimeList>
refreshes the page when a record is created, updated, or deleted.
import React from 'react';
import { Datagrid, TextField } from 'react-admin';
import { RealTimeList } from '@react-admin/ra-realtime';
const PostList = () => (
<RealTimeList>
<Datagrid>
<TextField source="title" />
</Datagrid>
</RealTimeList>
);
import React from "react";
import { Datagrid, TextField } from "react-admin";
import { RealTimeList } from "@react-admin/ra-realtime";
const PostList = () => (
<RealTimeList>
<Datagrid>
<TextField source="title" />
</Datagrid>
</RealTimeList>
);
To trigger refreshes of <RealTimeList>
, the API has to publish events containing at least the followings:
-
topic: '/resource/{resource}';
-
data : { topic : '/resource/{resource}', type: '{deleted || created || updated}', payload: { ids: [{listOfRecordIdentifiers}]}, }
The <RealTimeList>
allows you to customize the side effects triggered when it receives a new event, by passing a function to the onEventReceived
prop:
import { Datagrid, TextField, useNotify, useRefresh } from 'react-admin';
import { RealTimeList } from '@react-admin/ra-realtime';
const PostList = () => {
const notify = useNotify();
const refresh = useRefresh();
const handleEventReceived = event => {
const count = get(event, 'payload.ids.length', 1);
notify('ra-realtime.notification.list.refreshed', 'info', { count });
refresh();
};
return (
<RealTimeList onEventReceived={handleEventReceived}>
<Datagrid>
<TextField source="title" />
</Datagrid>
</RealTimeList>
);
};
import { Datagrid, TextField, useNotify, useRefresh } from "react-admin";
import { RealTimeList } from "@react-admin/ra-realtime";
const PostList = () => {
const notify = useNotify();
const refresh = useRefresh();
const handleEventReceived = (event) => {
const count = get(event, "payload.ids.length", 1);
notify("ra-realtime.notification.list.refreshed", "info", { count });
refresh();
};
return (
<RealTimeList onEventReceived={handleEventReceived}>
<Datagrid>
<TextField source="title" />
</Datagrid>
</RealTimeList>
);
};
<RealTimeEdit>
<RealTimeEdit>
displays a warning when the record is updated by another user and offers to refresh the page. Also, it displays a warning when the record is deleted by another user.
import React from 'react';
import { SimpleForm, TextInput } from 'react-admin';
import { RealTimeEdit } from '@react-admin/ra-realtime';
const PostEdit = props => (
<RealTimeEdit>
<SimpleForm>
<TextInput source="title" />
</SimpleForm>
</RealTimeEdit>
);
import React from "react";
import { SimpleForm, TextInput } from "react-admin";
import { RealTimeEdit } from "@react-admin/ra-realtime";
const PostEdit = (props) => (
<RealTimeEdit>
<SimpleForm>
<TextInput source="title" />
</SimpleForm>
</RealTimeEdit>
);
To trigger <RealTimeEdit>
features, the API has to publish events containing at least the following:
-
topic: '/resource/{resource}/{recordIdentifier}';
-
data : { topic : '/resource/{resource}/{recordIdentifier}', type: '{deleted || updated}', payload: { id: [{recordIdentifier}]}, }
The <RealTimeEdit>
allows you to customize the side effects triggered when it receives a new event, by passing a function to the onEventReceived
prop:
import React from 'react';
import { SimpleForm, TextInput, useRefresh } from 'react-admin';
import { RealTimeEdit } from '@react-admin/ra-realtime';
const PostEdit = () => {
const notify = useNotify();
const handleEventReceived = (
event,
{ setDeleted, setUpdated, setUpdatedDisplayed }
) => {
if (event.type === EventType.Updated) {
notify('ra-realtime.notification.record.updated', 'info');
setUpdated(true);
setUpdatedDisplayed(true);
} else if (event.type === EventType.Deleted) {
notify('ra-realtime.notification.record.deleted', 'info');
setDeleted(true);
setUpdated(false);
setUpdatedDisplayed(true);
}
};
return (
<RealTimeEdit onEventReceived={handleEventReceived}>
<SimpleForm>
<TextInput source="title" />
</SimpleForm>
</RealTimeEdit>
);
};
import React from "react";
import { SimpleForm, TextInput } from "react-admin";
import { RealTimeEdit } from "@react-admin/ra-realtime";
const PostEdit = () => {
const notify = useNotify();
const handleEventReceived = (event, { setDeleted, setUpdated, setUpdatedDisplayed }) => {
if (event.type === EventType.Updated) {
notify("ra-realtime.notification.record.updated", "info");
setUpdated(true);
setUpdatedDisplayed(true);
} else if (event.type === EventType.Deleted) {
notify("ra-realtime.notification.record.deleted", "info");
setDeleted(true);
setUpdated(false);
setUpdatedDisplayed(true);
}
};
return (
<RealTimeEdit onEventReceived={handleEventReceived}>
<SimpleForm>
<TextInput source="title" />
</SimpleForm>
</RealTimeEdit>
);
};
The function passed to onEventReceived
will be called with the event as its first argument and an object containing functions that will update the UI:
setDeleted
: If set totrue
, the edit view will show a message to let users know this record has been deleted.setUpdated
: If set totrue
, the edit view will show a message to let users know this record has been updated.setUpdatedDisplayed
: Must be set to true after callingsetUpdated
. This is used to show the message about the record being updated only for a few seconds.
<RealTimeShow>
<RealTimeShow>
shows a notification and refreshes the page when the record is updated by another user. Also, it displays a warning when the record is deleted by another user.
import React from 'react';
import { SimpleShowLayout, TextField } from 'react-admin';
import { RealTimeShow } from '@react-admin/ra-realtime';
const PostShow = () => (
<RealTimeShow>
<SimpleShowLayout>
<TextField source="title" />
</SimpleShowLayout>
</RealTimeShow>
);
import React from "react";
import { SimpleShowLayout, TextField } from "react-admin";
import { RealTimeShow } from "@react-admin/ra-realtime";
const PostShow = () => (
<RealTimeShow>
<SimpleShowLayout>
<TextField source="title" />
</SimpleShowLayout>
</RealTimeShow>
);
To trigger the <RealTimeShow>
updates, the API has to publish events containing at least the following:
-
topic: '/resource/{resource}/{recordIdentifier}';
-
data : { topic : '/resource/{resource}/{recordIdentifier}', type: '{deleted || updated}', payload: { id: [{recordIdentifier}]}, }
The <RealTimeShow>
allows you to customize the side effects triggered when it receives a new event, by passing a function to the onEventReceived
prop:
import { SimpleShowLayout, TextField, useRefresh } from 'react-admin';
import { RealTimeShow } from '@react-admin/ra-realtime';
const PostShow = () => {
const notify = useNotify();
const handleEventReceived = (event, { setDeleted }) => {
if (event.type === EventType.Updated) {
notify('ra-realtime.notification.record.updated', 'info');
refresh();
} else if (event.type === EventType.Deleted) {
notify('ra-realtime.notification.record.deleted', 'info');
setDeleted(true);
}
};
return (
<RealTimeShow onEventReceived={handleEventReceived}>
<SimpleShowLayout>
<TextField source="title" />
</SimpleShowLayout>
</RealTimeShow>
);
};
import { SimpleShowLayout, TextField } from "react-admin";
import { RealTimeShow } from "@react-admin/ra-realtime";
const PostShow = () => {
const notify = useNotify();
const handleEventReceived = (event, { setDeleted }) => {
if (event.type === EventType.Updated) {
notify("ra-realtime.notification.record.updated", "info");
refresh();
} else if (event.type === EventType.Deleted) {
notify("ra-realtime.notification.record.deleted", "info");
setDeleted(true);
}
};
return (
<RealTimeShow onEventReceived={handleEventReceived}>
<SimpleShowLayout>
<TextField source="title" />
</SimpleShowLayout>
</RealTimeShow>
);
};
The function passed to onEventReceived
will be called with the event as its first argument and an object containing functions that will update the UI:
setDeleted
: If set totrue
, the edit view will show a message to let users know this record has been deleted.
<RealTimeMenu>
The <RealTimeMenu>
component displays a badge with the number of updated records on each unactive Menu item.
import React from 'react';
import { Admin, Layout, LayoutProps, Resource } from 'react-admin';
import { RealTimeMenu } from '@react-admin/ra-realtime';
import { PostList, PostShow, PostEdit, realTimeDataProvider } from '.';
const CustomLayout = (props: LayoutProps) => (
<Layout {...props} menu={RealTimeMenu} />
);
const MyReactAdmin = () => (
<Admin dataProvider={realTimeDataProvider} layout={CustomLayout}>
<Resource
name="posts"
list={PostList}
show={PostShow}
edit={PostEdit}
/>
</Admin>
);
import React from "react";
import { Admin, Layout, Resource } from "react-admin";
import { RealTimeMenu } from "@react-admin/ra-realtime";
import { PostList, PostShow, PostEdit, realTimeDataProvider } from ".";
const CustomLayout = (props) => <Layout {...props} menu={RealTimeMenu} />;
const MyReactAdmin = () => (
<Admin dataProvider={realTimeDataProvider} layout={CustomLayout}>
<Resource name="posts" list={PostList} show={PostShow} edit={PostEdit} />
</Admin>
);
To trigger the <RealTimeMenu>
behavior, the API has to publish events containing at least the followings keys:
-
topic: '/resource/{resource}';
-
data : { topic : '/resource/{resource}', type: '{deleted || created || updated}', payload: { ids: [{listOfRecordIdentifiers}]}, }
<RealTimeMenuItemLink>
<RealTimeMenuItemLink>
displays a badge with the number of updated records if the current menu item is not active (Used to build <RealTimeMenu>
and your custom <MyRealTimeMenu>
).
import React from 'react';
import { MenuProps } from 'react-admin';
import { RealTimeMenuItemLink } from '@react-admin/ra-realtime';
const CustomRealTimeMenu = () => {
return (
<div>
<RealTimeMenuItemLink
to="/posts"
primaryText="The Posts"
resource="posts"
badgeColor="primary"
/>
<RealTimeMenuItemLink
to="/comments"
primaryText="The Comments"
resource="comments"
/>
</div>
);
};
import React from "react";
import { RealTimeMenuItemLink } from "@react-admin/ra-realtime";
const CustomRealTimeMenu = () => {
return (
<div>
<RealTimeMenuItemLink to="/posts" primaryText="The Posts" resource="posts" badgeColor="primary" />
<RealTimeMenuItemLink to="/comments" primaryText="The Comments" resource="comments" />
</div>
);
};
<RealTimeMenuItemLink>
has two additional props compared to <MenuItemLink>
:
resource
: Needed, The name of the concerned resource (can be different from the path in theto
prop)badgeColor
: Optional, It's the MUI color used to display the color of the badge. Default isalert
(not far from the red). It can also beprimary
,secondary
, or any of the MUI colors available in the MUI palette.
The badge displays the total number of changed records since the last time the <MenuItem>
opened. The badge value resets whenever the user opens the resource list page, and the <MenuItem>
becomes active.
To trigger <RealTimeMenuItemLink>
behavior, the API has to publish events containing at least the following elements:
-
{ topic: '/resource/{resource}'; }
-
{ data : { topic : '/resource/{resource}', type: '{deleted || created || updated}', payload: { ids: [{listOfRecordIdentifiers}]}, } }
useLockRecord
Locks a single record, so that it cannot be edited or deleted by another user.
This hook calls dataProvider.lock()
on mount and dataProvider.unlock()
on unmount to lock and unlock the record.
import { useLockRecord } from '@react-admin/ra-realtime';
// use this component e.g. in an `<Edit>` component
const MyLockStatus = () => {
const { isLocked } = useLockRecord();
return isLocked ? <div>You are locking this record</div> : null;
};
import { useLockRecord } from "@react-admin/ra-realtime";
// use this component e.g. in an `<Edit>` component
const MyLockStatus = () => {
const { isLocked } = useLockRecord();
return isLocked ? <div>You are locking this record</div> : null;
};
// additional parameters
const { isLocked, isLoading, error } = useLockRecord({
identity,
resource,
id,
meta,
lockMutationOptions,
unlockMutationOptions,
});
// additional parameters
const { isLocked, isLoading, error } = useLockRecord({
identity,
resource,
id,
meta,
lockMutationOptions,
unlockMutationOptions,
});
It accepts an options parameter with the following properties (all optional):
identity
: An identifier (string or number) corresponding to the identity of the locker (eg.'julien'
). This could be an authentication token for instance. Falls back to the identifier of the identity returned by theAuthProvider.getIdentity
function.resource
: The resource name (eg.'posts'
). The hook uses theResourceContext
if not provided.id
: The record id (eg.123
). The hook uses theRecordContext
if not provided.meta
: An object that will be forwarded to the dataProviderlockMutationOptions
:react-query
mutation options, used to customize the lock side-effects for instanceunlockMutationOptions
:react-query
mutation options, used to customize the unlock side-effects for instance
useGetLock
Gets the lock status for a record. It calls dataProvider.getLock()
on mount.
const { data, isLoading } = useGetLock(resource, { id });
const { data, isLoading } = useGetLock(resource, { id });
Parameters description:
resource
: the resource name (eg.'posts'
)id
: the record id (eg.123
)meta
: Optional. an object that will be forwarded to the dataProvider (optional)lockMutationOptions
: Optional. react-query mutation options, used to customize the lock side-effects for instanceunlockMutationOptions
: Optional. react-query mutation options, used to customize the unlock side-effects for instance
Here is a custom form Toolbar that displays the lock status of the current record:
const CustomToolbar = () => {
const resource = useResourceContext();
const record = useRecordContext();
const { isLoading: identityLoading, identity } = useGetIdentity();
const { isLoading: lockLoading, data: lock } = useGetLock(resource, {
id: record.id,
});
if (identityLoading || lockLoading) {
return null;
}
const isLockedByOtherUser = lock?.identity !== identity.id;
return (
<Toolbar>
<SaveButton disabled={isLockedByOtherUser} />
{isLockedByOtherUser && (
<LockMessage>
{`This record is locked by another user: ${lock?.dentity}.`}
</LockMessage>
)}
</Toolbar>
);
};
const CustomToolbar = () => {
const resource = useResourceContext();
const record = useRecordContext();
const { isLoading: identityLoading, identity } = useGetIdentity();
const { isLoading: lockLoading, data: lock } = useGetLock(resource, {
id: record.id,
});
if (identityLoading || lockLoading) {
return null;
}
const isLockedByOtherUser = lock?.identity !== identity.id;
return (
<Toolbar>
<SaveButton disabled={isLockedByOtherUser} />
{isLockedByOtherUser && (
<LockMessage>{`This record is locked by another user: ${lock?.dentity}.`}</LockMessage>
)}
</Toolbar>
);
};
useGetLocks
Get all the locks for a given resource.
// simple Usage
const { data } = useGetLocks('posts');
// simple Usage
const { data } = useGetLocks("posts");
Here is how to use it in a custom Datagrid, to disable edit and delete buttons for locked records:
const MyPostGrid = () => {
const resource = useResourceContext();
const { data: locks } = useGetLocks(resource);
return (
<Datagrid
bulkActionButtons={false}
sx={{
'& .MuiTableCell-root:last-child': {
textAlign: 'right',
},
}}
>
<MyPostTitle label="Title" locks={locks} />
<MyPostActions label="Actions" locks={locks} />
</Datagrid>
);
};
const MyPostTitle = ({ label, locks }: { label: string; locks: Lock[] }) => {
const record = useRecordContext();
const lock = locks.find(l => l.recordId === record.id);
return (
<WrapperField label={label}>
<TextField source="title" />
{lock && (
<span style={{ color: 'red' }}>
{` (Locked by ${lock.identity})`}
</span>
)}
</WrapperField>
);
};
const MyPostActions = ({ label, locks }: { label: string; locks: Lock[] }) => {
const record = useRecordContext();
const locked = locks.find(l => l.recordId === record.id);
return (
<WrapperField label={label}>
<DeleteButton disabled={!!locked} />
<LockableEditButton disabled={!!locked} />
</WrapperField>
);
};
const MyPostGrid = () => {
const resource = useResourceContext();
const { data: locks } = useGetLocks(resource);
return (
<Datagrid
bulkActionButtons={false}
sx={{
"& .MuiTableCell-root:last-child": {
textAlign: "right",
},
}}
>
<MyPostTitle label="Title" locks={locks} />
<MyPostActions label="Actions" locks={locks} />
</Datagrid>
);
};
const MyPostTitle = ({ label, locks }) => {
const record = useRecordContext();
const lock = locks.find((l) => l.recordId === record.id);
return (
<WrapperField label={label}>
<TextField source="title" />
{lock && <span style={{ color: "red" }}>{` (Locked by ${lock.identity})`}</span>}
</WrapperField>
);
};
const MyPostActions = ({ label, locks }) => {
const record = useRecordContext();
const locked = locks.find((l) => l.recordId === record.id);
return (
<WrapperField label={label}>
<DeleteButton disabled={!!locked} />
<LockableEditButton disabled={!!locked} />
</WrapperField>
);
};
useLock
and useUnlock
useLock(resource, { id, identity, meta }, options)
useUnlock(resource, { id, identity, meta }, options)
These hooks accept the following parameters:
identity
: an identifier (string or number) corresponding to the identity of the locker (eg.'julien'
). This could be an authentication token for instance.resource
: the resource name (eg.'posts'
)id
: the record id (eg.123
)meta
: an object that will be forwarded to the dataProvider (optional)options
: an object containing options for react-query (optional)
For most use cases, you won't need to call the useLock
and useUnlock
hooks directly, and should rather use the useLockRecord
orchestration hook which is responsible for calling these hooks for you. You may need to call useGetLock
and useGetLocks
however, as there is no orchestration hook for them.
Customizing Translation Messages
This module uses specific translations for displaying buttons and other texts. As for all translations in react-admin, it's possible to customize the messages.
To create your own translations, you can use the TypeScript types to see the structure and see which keys are overridable.
Here is an example of how to customize translations in your app:
import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
import frenchMessages from 'ra-language-french';
import {
TranslationMessages as BaseTranslationMessages,
raRealTimeEnglishMessages,
raRealTimeFrenchMessages,
RaRealTimeTranslationMessages,
} from '@react-admin/ra-realtime';
/* TranslationMessages extends the defaut translation
* Type from react-admin (BaseTranslationMessages)
* and the ra-realtime translation Type (RaRealTimeTranslationMessages)
*/
interface TranslationMessages
extends RaRealTimeTranslationMessages,
BaseTranslationMessages {}
const customEnglishMessages: TranslationMessages = mergeTranslations(
englishMessages,
raRealTimeEnglishMessages,
{
'ra-realtime': {
notification: {
record: {
updated: 'Wow, this entry has been modified by a ghost',
deleted: 'Hey, a ghost has stolen this entry',
},
list: {
refreshed:
'Be carefull, this list has been refreshed with %{smart_count} %{name} %{type} by some ghosts',
},
},
},
}
);
const i18nCustomProvider = polyglotI18nProvider(locale => {
if (locale === 'fr') {
return mergeTranslations(frenchMessages, raRealTimeFrenchMessages);
}
return customEnglishMessages;
}, 'en');
export const MyApp = () => (
<Admin dataProvider={myDataprovider} i18nProvider={i18nCustomProvider}>
...
</Admin>
);
import polyglotI18nProvider from "ra-i18n-polyglot";
import englishMessages from "ra-language-english";
import frenchMessages from "ra-language-french";
import { raRealTimeEnglishMessages, raRealTimeFrenchMessages } from "@react-admin/ra-realtime";
const customEnglishMessages = mergeTranslations(englishMessages, raRealTimeEnglishMessages, {
"ra-realtime": {
notification: {
record: {
updated: "Wow, this entry has been modified by a ghost",
deleted: "Hey, a ghost has stolen this entry",
},
list: {
refreshed:
"Be carefull, this list has been refreshed with %{smart_count} %{name} %{type} by some ghosts",
},
},
},
});
const i18nCustomProvider = polyglotI18nProvider((locale) => {
if (locale === "fr") {
return mergeTranslations(frenchMessages, raRealTimeFrenchMessages);
}
return customEnglishMessages;
}, "en");
export const MyApp = () => (
<Admin dataProvider={myDataprovider} i18nProvider={i18nCustomProvider}>
...
</Admin>
);
CHANGELOG
v4.0.2
2022-06-23
- (fix) useGetRecordId fails for falsy values (such as
0
)
v4.0.1
2022-06-08
- (fix) Update peer dependencies ranges (support React 18)
v4.0.0
2022-06-07
- Upgrade to react-admin v4
- Lock state management is now made with react-query instead of redux
- Make providing of current resource and recordId to hooks optional, since they can be fetched automatically using the context (hooks signatures has been changed accordingly, see below)
- Upgrade to MUI v5
- Add a new hook
useGetRecordId
to help with getting the recordId from either context or route - Fix
addRealTimeMethodsBasedOnMercure
: don't create an eventSource when there are no topics (avoids error 400) - Fix
addRealTimeMethodsBasedOnMercure
: fix missing promise return with lodash debounce addLocksMethodsBasedOnALockResource
:getLock
no longer rejects when there are no locks (instead resolves withundefined
)- Change dataProvider lock methods signatures, and add ability to handle a
meta
object
Breaking Changes
RealTimeEdit
, RealTimeList
and RealTimeShow
Changes To You no longer need to pass down the resource
and record
props to these components, as they will be fetched from context automatically.
import { RealTimeEdit } from '@react-admin/ra-realtime'
- const PostEdit = props => (
- <RealTimeEdit {...props}>
+ const PostEdit = () => (
+ <RealTimeEdit>
<SimpleForm>
<TextInput source="title" />
</SimpleForm>
</RealTimeEdit>
);
Also, the Warning
component, used to display alerts if the record has been changed by someone else, now makes use of MUI's Alert
component. This results in minor changes in appearance, and might also require you to change some classes name if you had custom styles for this component.
RealTimeMenuItemLink
Changes To The sidebarIsOpen
prop was removed, since this state is now determined using react-admin useSidebarState
.
Also, you no longer need to forward the onClick
prop to RealTimeMenuItemLink
if you don't have a custom callback. Clicking on a menu item will now close the menu on small screens, just like the community version of react-admin.
- const MyMenu: FC<any> = ({ onMenuClick }) => {
- const open = useSelector<ReduxState, boolean>(
- state => state.admin.ui.sidebarOpen
- );
+ const MyMenu = () => {
return (
<div>
<RealTimeMenuItemLink
to="/posts"
primaryText="The Posts"
resource="posts"
badgeColor="primary"
- onClick={onMenuClick}
- sidebarIsOpen={open}
/>
</div>
);
};
useSubscribeToRecord
Changes To You no longer need to pass down the current resource and recordId if they can be retrieved from context:
- useSubscribeToRecord('posts', props.id, () => {
+ useSubscribeToRecord(() => {
refresh();
notify('Record updated server-side');
});
If you need to pass them manually, you can still do so, but the order of the parameters has changed:
- useSubscribeToRecord('posts', 123, () => {
+ useSubscribeToRecord(() => {
refresh();
notify('Record updated server-side');
- });
+ }, 'posts', 123);
useSubscribeToRecordList
Changes To You no longer need to pass down the current resource if it can be retrieved from context:
- useSubscribeToRecordList('posts', () => {
+ useSubscribeToRecordList(() => {
refresh();
notify('list updated server-side');
});
If you need to pass it manually, you can still do so, but the order of the parameters has changed:
- useSubscribeToRecordList('posts', () => {
+ useSubscribeToRecordList(() => {
refresh();
notify('list updated server-side');
- });
+ }, 'posts');
useHasLock
Has Been Renamed To useGetLock
useHasLock
has been renamed to useGetLock
, and you no longer need to provide it with the current resource and recordId if they can be retrieved from context:
- const { loading, data: lock } = useHasLock(resource, recordId);
+ const { loading, data: lock } = useGetLock();
If you need to pass them manually, you can still do so, using the new hook signature:
- const { loading, data: lock } = useHasLock(resource, recordId);
+ const { loading, data: lock } = useGetLock(resource, { id: recordId });
useHasLocks
Has Been Renamed To useGetLocks
useHasLocks
has been renamed to useGetLocks
, and you no longer need to provide it with the current resource if it can be retrieved from context:
- const { data: locks } = useHasLocks(resource);
+ const { data: locks } = useGetLocks();
If you need to pass it manually, you can still do so:
- const { data: locks } = useHasLocks(resource);
+ const { data: locks } = useGetLocks(resource);
useLock
Has Been Added To Query The dataProvider.lock()
Function
Note: There was already a hook called useLock
, which has now been renamed to useLockRecord
, and is responsible for calling the new useLock
(as well as useUnlock
) for you. Please see below for more details about this hook.
useLock
has been added in order to query the dataProvider.lock()
function using react-query.
You need to provide this hook with the resource, recordId, and identity of the locker.
Example usage:
const [lock, _mutation] = useLock('posts', { id: 123, identity: 'mario' });
React.useEffect(() => {
lock();
}, []);
const [lock, _mutation] = useLock("posts", { id: 123, identity: "mario" });
React.useEffect(() => {
lock();
}, []);
In most use cases, you would not want to use this hook directly, but rather the new hook useLockRecord
(see below).
useUnlock
Has Been Added To Query The dataProvider.unlock()
Function
useUnlock
has been added in order to query the dataProvider.unlock()
function using react-query.
You need to provide this hook with the resource, recordId, and identity of the locker.
Example usage:
const [unlock, _mutation] = useUnlock('posts', { id: 123, identity: 'mario' });
React.useEffect(() => {
unlock();
}, []);
const [unlock, _mutation] = useUnlock("posts", { id: 123, identity: "mario" });
React.useEffect(() => {
unlock();
}, []);
In most use cases, you would not want to use this hook directly, but rather the new hook useLockRecord
(see below).
useLock
Hook Has Been Renamed To useLockRecord
Old The previous useLock
hook has been renamed to useLockRecord
, and its signature has changed.
This hook is responsible for orchestrating the lock and unlock of a record (which was previously accomplished by useLock
), calling the new useLock
and useUnlock
hooks internally.
Below is an example showing how to replace the old hook by the new one:
- const { loading } = useLock(resource, recordId, 'mario', {
- onSuccess: () => {
- notify('ra-realtime.notification.lock.lockedByMe');
- },
- onFailure: () => {
- notify('ra-realtime.notification.lock.lockedBySomeoneElse');
- },
- onUnlockSuccess: () => {
- notify('ra-realtime.notification.lock.unlocked');
- },
- });
+ const { isLocked, isLoading: loading } = useLockRecord({
+ identity: 'mario',
+ resource: resource,
+ id: recordId,
+ lockMutationOptions: {
+ onSuccess: () => {
+ notify('ra-realtime.notification.lock.lockedByMe');
+ },
+ onError: () => {
+ notify('ra-realtime.notification.lock.lockedBySomeoneElse');
+ },
+ },
+ unlockMutationOptions: {
+ onSuccess: () => {
+ notify('ra-realtime.notification.lock.unlocked');
+ },
+ },
+ });
Just like with useGetLock
, you do not need to provide this hook with the current resource and recordId if they can be retrieved from context.
Below is the same example as before, without providing the resource
and recordId
:
- const { loading } = useLock(resource, recordId, 'mario', {
- onSuccess: () => {
- notify('ra-realtime.notification.lock.lockedByMe');
- },
- onFailure: () => {
- notify('ra-realtime.notification.lock.lockedBySomeoneElse');
- },
- onUnlockSuccess: () => {
- notify('ra-realtime.notification.lock.unlocked');
- },
- });
+ const { isLocked, isLoading: loading } = useLockRecord({
+ identity: 'mario',
+ lockMutationOptions: {
+ onSuccess: () => {
+ notify('ra-realtime.notification.lock.lockedByMe');
+ },
+ onError: () => {
+ notify('ra-realtime.notification.lock.lockedBySomeoneElse');
+ },
+ },
+ unlockMutationOptions: {
+ onSuccess: () => {
+ notify('ra-realtime.notification.lock.unlocked');
+ },
+ },
+ });
dataProvider.getLock
Method
Changes To The The getLock
method will now be called like so:
await locksDataProvider.getLock('post', {
- recordId: 143,
+ id: 143,
});
Also, please note that the getLock
method must now return a Promise<GetLockResult>
.
dataProvider.getLocks
Method
Changes To The The signature of the getLocks
method has not changed.
However, please note that the getLocks
method must now return a Promise<GetLocksResult>
.
dataProvider.lock
Method
Changes To The The lock
method will now be called like so:
await locksDataProvider.lock('post', {
- recordId: 143,
+ id: 143,
identity: 'adrien' // It could be an authentication token
});
Also, please note that the lock
method must now return a Promise<LockResult>
.
dataProvider.unlock
Method
Changes To The The unlock
method will now be called like so:
await locksDataProvider.unlock('post', {
- recordId: 143,
+ id: 143,
identity: 'adrien' // It could be an authentication token
});
No changes were made to the return type.
v1.4.3
2021-09-16
- (fix) Fix usage with latest Mercure version
v1.4.2
2021-07-20
- (fix) Remove dependency to material-ui lab
v1.4.1
2021-06-29
- (fix) Update peer dependencies ranges (support react 17)
v1.4.0
2021-05-25
- (feat) Provide custom event handlers helper functions to update the views state.
Example for the Edit View
import React from 'react';
import { SimpleForm, TextInput, useRefresh } from 'react-admin';
import { RealTimeEdit } from '@react-admin/ra-realtime';
const PostEdit = props => {
const notify = useNotify();
const handleEventReceived = (
event,
{ setDeleted, setUpdated, setUpdatedDisplayed }
) => {
if (event.type === EventType.Updated) {
notify('ra-realtime.notification.record.updated', 'info');
setUpdated(true);
setUpdatedDisplayed(true);
} else if (event.type === EventType.Deleted) {
notify('ra-realtime.notification.record.deleted', 'info');
setDeleted(true);
setUpdated(false);
setUpdatedDisplayed(true);
}
};
return (
<RealTimeEdit {...props} onEventReceived={handleEventReceived}>
<SimpleForm>
<TextInput source="title" />
</SimpleForm>
</RealTimeEdit>
);
};
import React from "react";
import { SimpleForm, TextInput } from "react-admin";
import { RealTimeEdit } from "@react-admin/ra-realtime";
const PostEdit = (props) => {
const notify = useNotify();
const handleEventReceived = (event, { setDeleted, setUpdated, setUpdatedDisplayed }) => {
if (event.type === EventType.Updated) {
notify("ra-realtime.notification.record.updated", "info");
setUpdated(true);
setUpdatedDisplayed(true);
} else if (event.type === EventType.Deleted) {
notify("ra-realtime.notification.record.deleted", "info");
setDeleted(true);
setUpdated(false);
setUpdatedDisplayed(true);
}
};
return (
<RealTimeEdit {...props} onEventReceived={handleEventReceived}>
<SimpleForm>
<TextInput source="title" />
</SimpleForm>
</RealTimeEdit>
);
};
Example for the Show View
import { SimpleShowLayout, TextField, useRefresh } from 'react-admin';
import { RealTimeShow } from '@react-admin/ra-realtime';
const PostShow = props => {
const notify = useNotify();
const handleEventReceived = (event, { setDeleted }) => {
if (event.type === EventType.Updated) {
notify('ra-realtime.notification.record.updated', 'info');
refresh();
} else if (event.type === EventType.Deleted) {
notify('ra-realtime.notification.record.deleted', 'info');
setDeleted(true);
}
};
return (
<RealTimeShow {...props} onEventReceived={handleEventReceived}>
<SimpleShowLayout>
<TextField source="title" />
</SimpleShowLayout>
</RealTimeShow>
);
};
import { SimpleShowLayout, TextField } from "react-admin";
import { RealTimeShow } from "@react-admin/ra-realtime";
const PostShow = (props) => {
const notify = useNotify();
const handleEventReceived = (event, { setDeleted }) => {
if (event.type === EventType.Updated) {
notify("ra-realtime.notification.record.updated", "info");
refresh();
} else if (event.type === EventType.Deleted) {
notify("ra-realtime.notification.record.deleted", "info");
setDeleted(true);
}
};
return (
<RealTimeShow {...props} onEventReceived={handleEventReceived}>
<SimpleShowLayout>
<TextField source="title" />
</SimpleShowLayout>
</RealTimeShow>
);
};
v1.3.2
2021-04-26
- (performances) Replace MUI boxes by div with styles.
v1.3.1
2021-03-18
- (fix) Disable List Notifications
v1.3.0
2021-02-18
- (feat) Allows to customize side effects when an event is received in RealTimeList, RealTimeEdit and RealTimeShow via the
onEventReceived
prop.
import { useRefresh } from 'react-admin';
import { RealTimeList } from '@react-admin/ra-realtime';
const PostList = props => {
const refresh = useRefresh();
const handleEventReceived = event => {
refresh();
};
return (
<RealTimeList {...props} onEventReceived={handleEventReceived}>
<Datagrid>
<TextField source="title" />
</Datagrid>
</RealTimeList>
);
};
v1.2.1
2020-11-18
- Upgrade to react-admin
3.10
v1.2.0
2020-10-05
- Upgrade to react-admin
3.9
v1.1.3
2020-09-30
- Update Readme
v1.1.2
2020-09-30
- (fix) Fix bad export in realtime Edit Storybook Action
v1.1.1
2020-09-28
- (fix) Fix extra spacing in storybook edit examples
v1.1.0
2020-09-21
- (feat) Add the autoclaim capability (autolock when unlocked) to the
useLock
hook - (fix) Fix missing storybook examples for realtime locking
v1.0.5
2020-09-18
- (fix) Fix non-working Mercure storybook examples
v1.0.4
2020-09-15
- (fix) Fix missing export
- (deps) Upgrade dependencies
v1.0.3
2020-09-14
- (feat) Add a
useHasLocks
hook to select all locks for a resource
v1.0.2
2020-09-10
- (fix) Add missing resource security check on the
useHasLock
selector
v1.0.0
2020/08/04
- First release