Skip to main content

Core

NPM | GitHub

npm install @civet/core

The core module provides Civet's base functionality.

ConfigContext

React context for providing shared configuration to core components.

Context

NameTypeDescription
dataProviderDataProvider

<ConfigProvider>, <ConfigConsumer>, useConfigContext

<ConfigProvider>

Context provider for the ConfigContext.

<ConfigProvider dataProvider={provider}>...</ConfigProvider>

Props

NameTypeDescription
dataProviderDataProvider

ConfigContext, <ConfigConsumer>, useConfigContext

<ConfigConsumer>

Context consumer for the ConfigContext.

<ConfigConsumer>
{(context) => ...}
</ConfigConsumer>

ConfigContext, <ConfigProvider>, useConfigContext

useConfigContext

Context consumer for the ConfigContext.

const context = useConfigContext();

Function arguments

None

Return type

TypeDescription
objectConfigContext

ConfigContext, <ConfigProvider>, <ConfigConsumer>

ResourceContext

React context for providing resource state, usually provided by <Resource>.

Context

NameTypeDescription
namestringResource name
queryanyQuery instructions
optionsobjectQuery options for requests
dataProviderDataProviderDataProvider to be used for requests
requeststringUnique identifier for the current request - the value can be compared alphanumerically
revisionstringUnique identifier for the current request's revision - the value can be compared alphanumerically
dataany[]The actual data
metaobjectMeta information
errorError | booleanError information about the most recent request, or true if no further details are available
isEmptybooleanWhether fetching data is disabled, resulting in an empty data array
isLoadingbooleanWhether another query is currently being executed
isIncompletebooleanWhether the current query is still being executed
isInitialbooleanWhether the current query is the first non failing query
isStalebooleanWhether the current data is stale
next{ request: string, revision: string }Information about the next query
notify() => Promise<{ request: string, revision: string }>Callback to reload the current request - Returns a Promise with the resulting request and revision

<Resource>, useResource, <ResourceProvider>, <ResourceConsumer>, useResourceContext

<Resource>

Provides data based on the given request details and DataProvider. Context provider for the ResourceContext.

Necessary configuration that is not directly specified is taken from the ConfigContext.

There is also a useResource hook available with similar functionality.

warning

The provided DataProvider must not be changed.

<Resource name="persons" query={{ city: "New York" }}>
...
</Resource>

Props

NameTypeDescription
namestring (required)Resource name
queryanyQuery instructions
optionsobjectQuery options for requests
emptybooleanDisables fetching data, resulting in an empty data array
persistentboolean | "very"Whether stale data should be retained during the next request - this only applies if name did not change, unless set to "very"
dataProviderDataProviderDataProvider to be used for requests - must not be changed

ResourceContext, useResource, <ResourceProvider>, <ResourceConsumer>, useResourceContext

useResource

Provides data based on the given request details and DataProvider. Can be used with ResourceProvider.

Necessary configuration that is not directly specified is taken from the ConfigContext.

There is also a <Resource> component available with similar functionality.

warning

The provided DataProvider must not be changed.

const context = useResource({ name: "persons", query: { city: "New York" } });

Function arguments

NameTypeDescription
configobjectResource configuration

Resource configuration

NameTypeDescription
namestring (required)Resource name
queryanyQuery instructions
optionsobjectQuery options for requests
emptybooleanDisables fetching data, resulting in an empty data array
persistentboolean | "very"Whether stale data should be retained during the next request - this only applies if name did not change, unless set to "very"
dataProviderDataProviderDataProvider to be used for requests - must not be changed

Return type

TypeDescription
objectResourceContext

ResourceContext, <Resource>, <ResourceProvider>, <ResourceConsumer>, useResourceContext

<ResourceProvider>

Context provider for the ResourceContext.

In most cases, it is better to use <Resource> or useResource instead.

const context = useResource(...);

<ResourceProvider value={context}>...</ResourceProvider>

Props

NameTypeDescription
valueResourceContext

ResourceContext, <Resource>, useResource, <ResourceConsumer>, useResourceContext

<ResourceConsumer>

Context consumer for the ResourceContext.

<ResourceConsumer>
{(context) => ...}
</ResourceConsumer>

ResourceContext, <Resource>, useResource, <ResourceProvider>, useResourceContext

useResourceContext

Context consumer for the ResourceContext.

const context = useResourceContext();

Function arguments

None

Return type

TypeDescription
objectResourceContext

ResourceContext, <Resource>, useResource, <ResourceProvider>, <ResourceConsumer>

DataProvider

Base class for implementing your own DataProvider.

class CustomProvider extends DataProvider {
handleGet(resource, query, options, meta) {
return ...;
}

...
}

const provider = new CustomProvider();

Class members

NameArgumentsReturn TypeDescription
getresourceName: string, query: any, options: object, meta: object | Meta, abortSignal: AbortSignalPromise<any[]>Get data (at once | uses handleGet internally)
continuousGetresourceName: string, query: any, options: object, meta: object | Meta, callback: (error: any, complete: boolean, data: any[]) => void, abortSignal: AbortSignalvoidGet data (continuously | uses handleGet internally)
createresourceName: string, data: any, options: object, meta: object | MetaPromise<any>Create data (uses handleCreate internally)
updateresourceName: string, query: any, data: any, options: object, meta: object | MetaPromise<any>Update data (uses handleUpdate internally)
patchresourceName: string, query: any, data: any, options: object, meta: object | MetaPromise<any>Patch data (uses handlePatch internally)
removeresourceName: string, query: any, options: object, meta: object | MetaPromise<any>Remove data (uses handleRemove internally)
subscriberesourceName: string, handler: () => voidunsubscribe: () => voidSubscribe to data change notifications for the specified resourceName
notifyresourceName: stringvoidNotify data changes for the specified resourceName (if no resourceName is specified, all subscribers are notified)
extend{ context: (contextPlugin: ReactHook) => void, ui: (uiPlugin: ReactComponent) => void }voidExtend Civet with custom functionality (see Extending Civet for more details)
createInstanceanyCreates an instance element for storing information that needs to be preserved over the lifetime of a consumer (available via meta.instance if provided)
releaseInstanceanyvoidUsed to release an instance element previously created with createInstance
compareRequestsnextRequestDetails: object, prevRequestDetails: objectbooleanCompare requests for equality - can be used to customize which changes to a resource's configuration cause a new request to be created
shouldPersistnextRequestDetails: object, prevRequestDetails: object, persistent: boolean | "very"booleanCompare requests for persistency - can be used to customize which changes to a resource's configuration cause the resource state to be persisted
compareItemVersionsnextItem: any, prevItem: anybooleanCompare two item versions for equality (used in recycleItems) - return true if the item was not changed at all, e.g. both versions are completely equal. (You can do so by comparing ETags or similar if available)
getItemIdentifieritem: anystringDetermine an item's unique identifier (used in recycleItems) - should return a string which is uniquely identifying the same item across multiple requests.
transitionnextContext: object, prevContext: objectany[]Transition between the previous and current data array (see caveats for more details)
recycleItemsnextContext: object, prevContext: objectany[]Recycle unchanged items (memoization) to prevent unnecessary rerenders (see caveats for more details)

Abstract members

NameArgumentsReturn TypeDescription
handleGetresourceName: string, query: any, options: object, meta: Meta, abortSignal: AbortSignalany[] | Promise<any[]> | (callback: (error: any, complete: boolean, data: any[]) => void) => voidA callback function can be returned to support continuous gets (see caveats for more details)
handleCreateresourceName: string, data: any, options: object, meta: Metaany | Promise<any>
handleUpdateresourceName: string, query: any, data: any, options: object, meta: Metaany | Promise<any>
handlePatchresourceName: string, query: any, data: any, options: object, meta: Metaany | Promise<any>
handleRemoveresourceName: string, query: any, options: object, meta: Metaany | Promise<any>

Caveats

Abstract functions

The functions get, create, ... internally invoke their corresponding abstract counterparts handle... and perform generic validation on their parameters and return values. Therefore, you should not override them, but implement the abstract handle... methods instead.

Meta information

The meta attribute provided to DataProvider's functions can have multiple applications. It is an interface which can be used to pass additional meta information beside the actual data to its consumers.

When a get request is made by the <Resource> component or useResource hook, the meta object has the following functions:

  • Its contents are published to the consumers of the resource via the ResourceContext.
  • Its contents are preserved between multiple revisions of a request and thereas can be used to provide information to subsequent queries or utility functions like transition or recycleItems. When the resource is in persistent mode, the information is also preserved between multiple requests.

As the contents of meta may be preserved between multiple requests or revisions, it may be necessary to clean them up in your DataProvider's get function. Please see Meta for more information.

continuousGet & transitioning

dataProvider.get resolves when the complete data is collected. This is fine when resolving the data is really fast but can be troublesome when you want to load large data sets e.g. from a backend over a slow internet connection. You can implement your DataProvider to support continuous data fetching by returning a callback function from handleGet rather than the data itself. This allows you to publish incomplete data to a resource (or other compatible clients) even if the fetch has not yet been completed.

It can be helpful to allow a transition between several updates of the component, e.g. to keep the order of the elements while the retrieval is still running. The <Resource> component and useResource hook support this by calling the DataProvider's transition method each time it resolves new data. The function is called before recycleItems, so you don't have to worry about memoization when implementing the transition.

recycleItems

React offers tools to avoid unnecessary component updates, for example shouldComponentUpdate, PureComponent and memo. These tools check whether the props of a component have changed since the previous render to determine if the component needs to render again. The fastest way to achieve this would be to use Object.is, which behaves like JavaScript's strict comparison operator === except for a few differences. This function works great for primitives like strings or numbers, but doesn't work like we would expect it to when used with objects and arrays. This is because objects and arrays (which in fact are objects as well) are compared by their memory addresses instead of their contents. See the example below:

const a = { x: 1 };
const b = { x: 1 };
const c = a;
a === b; // -> false: not the same memory address
a === c; // -> true: same memory address

recycleItems attempts to fix this issue. It is internally called by the <Resource> component and useResource hook after each fetch. The function compares the previous items with the next ones and attempts to reapply all unchanged items from the previous array to the new one. As a result, the following checks should succeed:

  • array equality
    • if one or more items differ (compared by value): prevData !== nextData
    • if items were added or removed: prevData !== nextData
    • if the order of the arrays differs: prevData !== nextData
    • else: prevData === nextData
  • item equality
    • if an item differs (compared by value): prevItem !== nextItem
    • else (even if it was reordered in the array): prevItem === nextItem

However, the default implementation may be expensive in regard to performance and may be inaccurate as it creates a hash over each item as its unique identifiers. This is why, if possible, you should improve it with a faster comparing algorithm by overriding the methods getItemIdentifier and compareItemVersions. You can also completely ditch the default implementation by implementing your own version of recycleItems. This is required in cases where you cannot determine a unique identifier per item.

isDataProvider

Identifies DataProvider instances.

const provider = new DataProvider();

if (!isDataProvider(provider)) {
throw new Error("Should be a DataProvider instance");
}

Function arguments

NameTypeDescription
dataProvideranyThe element to be checked

Return type

TypeDescription
booleanWhether dataProvider is an instance of DataProvider

dataProviderPropType

PropType for DataProvider instances.

const propTypes = {
optional: dataProviderPropType,
required: dataProviderPropType.isRequired,
};

createPlugin

Creates a plugin from the provided configuration function.

See Extending Civet for further details.

const plugin = createPlugin((BaseDataProvider) => {
function useMyContextPlugin(context, props) {
return context;
}

class ExtendedDataProvider extends BaseDataProvider {
extend(extend) {
super.extend(extend);

// Register a context plugin
extend.context(useMyContextPlugin);
}

// ...
}

return ExtendedDataProvider;
});

const DataProviderWithPlugin = plugin(SomeDataProvider);

Function arguments

NameTypeDescription
pluginDefinition(BaseDataProvider: DataProvider) => DataProviderA function that returns an extended version of BaseDataProvider

Return type

TypeDescription
(DataProvider) => DataProviderThe plugin

compose

Composes the specified single-argument functions from right to left.

This can be especially useful when applying multiple plugins to a DataProvider.

const DataProviderWithPlugins = compose(
pluginA,
pluginB,
pluginC
)(SomeDataProvider);
// This is the same as: pluginA(pluginB(pluginC(SomeDataProvider)))

Function arguments

NameTypeDescription
...fns(a: any) => anyThe functions to be composed

Return type

TypeDescription
(a: any) => anyThe composed functions

Notifier

Interface for handling client side notification events.

// Basic usage
const notifier = new Notifier();
function handler() {
console.log("Subscriber was notified");
}
const unsubscribeHandler = notifier.subscribe(handler);
console.log(notifier.isSubscribed(handler)); // true
notifier.trigger();
unsubscribeHandler();
console.log(notifier.isSubscribed(handler)); // false

// You can pass arguments to the handlers
const notifier = new Notifier();
notifier.subscribe((a, b, c) => {
console.log("Notified:", a, b, c);
});
notifier.trigger(true, 2, "test");

Class members

NameArgumentsReturn TypeDescription
subscribehandler: (...args: any) => voidunsubscribe: () => voidSubscribe to notifications
isSubscribedhandler: (...args: any) => voidbooleanWhether the provided handler is currently subscribed to the notifier
trigger...args: anyvoidNotify all currently subscribed handlers

ChannelNotifier

Notifier that supports multiple separate event channels.

// Basic usage
const notifier = new ChannelNotifier();
function handler() {
console.log("Subscriber was notified");
}
const unsubscribeHandlerFromChA = notifier.subscribe("channel-a", handler);
console.log(notifier.isSubscribed("channel-a", handler)); // true
console.log(notifier.isSubscribed("channel-b", handler)); // false
notifier.trigger("channel-a"); // Only notify channel a
notifier.trigger(); // Notify all channels
unsubscribeHandlerFromChA();
console.log(notifier.isSubscribed("channel-a", handler)); // false

// You can pass arguments to the handlers
const notifier = new Notifier();
notifier.subscribe("channel-a", (a, b, c) => {
console.log("Notified:", a, b, c);
});
notifier.trigger("channel-a", true, 2, "test"); // Notify all channels
notifier.trigger(null, true, 2, "test"); // Notify all channels
// Please note that the channel name is not passed to handlers by default.

Class members

NameArgumentsReturn TypeDescription
subscribehandler: (channel: string, ...args: any) => voidunsubscribe: () => voidSubscribe to notifications on the specified channel
isSubscribedhandler: (channel: string, ...args: any) => voidbooleanWhether the provided handler is currently subscribed to the specified channel
triggerchannel: string, ...args: anyvoidNotify all handlers currently subscribed to the specified channel (Set the channel to undefined or null to notify all channels)

AbortSignal

Interface for handling abort requests.

// Basic usage
const signal = new AbortSignal();
signal.listen(() => {
console.log("Request has been aborted");
});
signal.abort();
signal.listen(() => {
console.log(
"This will be called immediately, as signal has already been aborted"
);
});

// The signal can be locked if it is now longer allowed to be aborted
const signal = new AbortSignal();
signal.lock();
signal.abort(); // No listeners will be notified since the signal is already locked

Class members

NameArgumentsReturn TypeDescription
listenlistener: () => voidunsubscribe: () => voidListen for abort signals
abortvoidAbort the signal
lockvoidLock the signal (The signal can no longer be aborted)
proxy{ listen, locked, aborted }Creates a readonly proxy for the signal

Class variables

NameTypeDescription
lockedbooleanWhether the signal is locked
abortedbooleanWhether the signal is aborted

Meta

Meta information key value map.

// Basic usage
const meta = new Meta();
meta.set("test", 1);
const result = meta.commit();
console.log(result.test);

// Meta can be based on an existing object
const base = {};
const baseMeta = new Meta(base);
baseMeta.set("test", 1);
assert(base.test === baseMeta.get("test"));

// Meta can create (shallowly) immutable snapshots.
const previous = { a: 1 };
const meta = new Meta();
meta.set("a", 1);
const unchanged = meta.commit(previous);
meta.set("a", 2);
const changed = meta.commit(previous);
assert(previous === unchanged);
assert(previous !== changed);

Constructor

ArgumentsDescription
base: objectAll changes get applied to base if it is set
instance: anyInstance element for the meta object

Class members

NameArgumentsReturn TypeDescription
clearvoidDelete all keys from the object
deletekey: stringanyDelete the specified key from the object - returns the deleted value
entries([key: string, value: any])[]Get all entries from the object
getkey: stringanyGet the value for the specified key from the object
haskey: stringbooleanCheck if the object has a value for the specified key
keysstring[]Get all keys from the object
setkey: string, value: anyvoidSet the value for the specified key. Make sure that values are immutable!
valuesany[]Get all values from the object
commitprev: objectobjectGet the object as a plain JavaScript object - returns a shallow copy of the current value, or prev if provided and if all keys match

Class variables

NameTypeDescription
instanceanyThe instance element if provided by the constructor

Caveats

Immutability

Meta does NOT make sure that the values you provide are immutable. If you mutate an array or object which you previously passed to a Meta object, the change is also reflected in this Meta object, even when committed. Commit only creates a shallow copy of the base object. To guarantee that your values are truely immutable, it is recommended to use a library like immer or Immutable.js.