Core
npm install @civet/core
The core module provides Civet's base functionality.
ConfigContext
React context for providing shared configuration to core components.
Context
Name | Type | Description |
---|---|---|
dataProvider | DataProvider |
Related
<ConfigProvider>
, <ConfigConsumer>
, useConfigContext
<ConfigProvider>
Context provider for the ConfigContext
.
- Usage
- Import
<ConfigProvider dataProvider={provider}>...</ConfigProvider>
import { ConfigProvider } from "@civet/core";
Props
Name | Type | Description |
---|---|---|
dataProvider | DataProvider |
Related
ConfigContext
, <ConfigConsumer>
, useConfigContext
<ConfigConsumer>
Context consumer for the ConfigContext
.
- Usage
- Import
<ConfigConsumer>
{(context) => ...}
</ConfigConsumer>
import { ConfigConsumer } from "@civet/core";
Related
ConfigContext
, <ConfigProvider>
, useConfigContext
useConfigContext
Context consumer for the ConfigContext
.
- Usage
- Import
const context = useConfigContext();
import { useConfigContext } from "@civet/core";
Function arguments
None
Return type
Type | Description |
---|---|
object | ConfigContext |
Related
ConfigContext
, <ConfigProvider>
, <ConfigConsumer>
ResourceContext
React context for providing resource state, usually provided by <Resource>
.
Context
Name | Type | Description |
---|---|---|
name | string | Resource name |
query | any | Query instructions |
options | object | Query options for requests |
dataProvider | DataProvider | DataProvider to be used for requests |
request | string | Unique identifier for the current request - the value can be compared alphanumerically |
revision | string | Unique identifier for the current request's revision - the value can be compared alphanumerically |
data | any[] | The actual data |
meta | object | Meta information |
error | Error | boolean | Error information about the most recent request, or true if no further details are available |
isEmpty | boolean | Whether fetching data is disabled, resulting in an empty data array |
isLoading | boolean | Whether another query is currently being executed |
isIncomplete | boolean | Whether the current query is still being executed |
isInitial | boolean | Whether the current query is the first non failing query |
isStale | boolean | Whether 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 |
Related
<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.
The provided DataProvider
must not be changed.
- Usage
- Import
<Resource name="persons" query={{ city: "New York" }}>
...
</Resource>
import { Resource } from "@civet/core";
Props
Name | Type | Description |
---|---|---|
name | string (required) | Resource name |
query | any | Query instructions |
options | object | Query options for requests |
empty | boolean | Disables fetching data, resulting in an empty data array |
persistent | boolean | "very" | Whether stale data should be retained during the next request - this only applies if name did not change, unless set to "very" |
dataProvider | DataProvider | DataProvider to be used for requests - must not be changed |
Related
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.
The provided DataProvider
must not be changed.
- Usage
- Import
const context = useResource({ name: "persons", query: { city: "New York" } });
import { useResource } from "@civet/core";
Function arguments
Name | Type | Description |
---|---|---|
config | object | Resource configuration |
Resource configuration
Name | Type | Description |
---|---|---|
name | string (required) | Resource name |
query | any | Query instructions |
options | object | Query options for requests |
empty | boolean | Disables fetching data, resulting in an empty data array |
persistent | boolean | "very" | Whether stale data should be retained during the next request - this only applies if name did not change, unless set to "very" |
dataProvider | DataProvider | DataProvider to be used for requests - must not be changed |
Return type
Type | Description |
---|---|
object | ResourceContext |
Related
ResourceContext
, <Resource>
, <ResourceProvider>
, <ResourceConsumer>
, useResourceContext
<ResourceProvider>
Context provider for the ResourceContext
.
In most cases, it is better to use <Resource>
or useResource
instead.
- Usage
- Import
const context = useResource(...);
<ResourceProvider value={context}>...</ResourceProvider>
import { ResourceProvider } from "@civet/core";
Props
Name | Type | Description |
---|---|---|
value | ResourceContext |
Related
ResourceContext
, <Resource>
, useResource
, <ResourceConsumer>
, useResourceContext
<ResourceConsumer>
Context consumer for the ResourceContext
.
- Usage
- Import
<ResourceConsumer>
{(context) => ...}
</ResourceConsumer>
import { ResourceConsumer } from "@civet/core";
Related
ResourceContext
, <Resource>
, useResource
, <ResourceProvider>
, useResourceContext
useResourceContext
Context consumer for the ResourceContext
.
- Usage
- Import
const context = useResourceContext();
import { useResourceContext } from "@civet/core";
Function arguments
None
Return type
Type | Description |
---|---|
object | ResourceContext |
Related
ResourceContext
, <Resource>
, useResource
, <ResourceProvider>
, <ResourceConsumer>
DataProvider
Base class for implementing your own DataProvider.
- Usage
- Import
class CustomProvider extends DataProvider {
handleGet(resource, query, options, meta) {
return ...;
}
...
}
const provider = new CustomProvider();
import { DataProvider } from "@civet/core";
Class members
Name | Arguments | Return Type | Description |
---|---|---|---|
get | resourceName: string , query: any , options: object , meta: object | Meta , abortSignal: AbortSignal | Promise<any[]> | Get data (at once | uses handleGet internally) |
continuousGet | resourceName: string , query: any , options: object , meta: object | Meta , callback: (error: any, complete: boolean, data: any[]) => void , abortSignal: AbortSignal | void | Get data (continuously | uses handleGet internally) |
create | resourceName: string , data: any , options: object , meta: object | Meta | Promise<any> | Create data (uses handleCreate internally) |
update | resourceName: string , query: any , data: any , options: object , meta: object | Meta | Promise<any> | Update data (uses handleUpdate internally) |
patch | resourceName: string , query: any , data: any , options: object , meta: object | Meta | Promise<any> | Patch data (uses handlePatch internally) |
remove | resourceName: string , query: any , options: object , meta: object | Meta | Promise<any> | Remove data (uses handleRemove internally) |
subscribe | resourceName: string , handler: () => void | unsubscribe: () => void | Subscribe to data change notifications for the specified resourceName |
notify | resourceName: string | void | Notify data changes for the specified resourceName (if no resourceName is specified, all subscribers are notified) |
extend | { context: (contextPlugin: ReactHook) => void, ui: (uiPlugin: ReactComponent) => void } | void | Extend Civet with custom functionality (see Extending Civet for more details) |
createInstance | any | Creates an instance element for storing information that needs to be preserved over the lifetime of a consumer (available via meta.instance if provided) | |
releaseInstance | any | void | Used to release an instance element previously created with createInstance |
compareRequests | nextRequestDetails: object , prevRequestDetails: object | boolean | Compare requests for equality - can be used to customize which changes to a resource's configuration cause a new request to be created |
shouldPersist | nextRequestDetails: object , prevRequestDetails: object , persistent: boolean | "very" | boolean | Compare requests for persistency - can be used to customize which changes to a resource's configuration cause the resource state to be persisted |
compareItemVersions | nextItem: any , prevItem: any | boolean | Compare 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) |
getItemIdentifier | item: any | string | Determine an item's unique identifier (used in recycleItems ) - should return a string which is uniquely identifying the same item across multiple requests. |
transition | nextContext: object , prevContext: object | any[] | Transition between the previous and current data array (see caveats for more details) |
recycleItems | nextContext: object , prevContext: object | any[] | Recycle unchanged items (memoization) to prevent unnecessary rerenders (see caveats for more details) |
Abstract members
Name | Arguments | Return Type | Description |
---|---|---|---|
handleGet | resourceName: string , query: any , options: object , meta: Meta , abortSignal: AbortSignal | any[] | Promise<any[]> | (callback: (error: any, complete: boolean, data: any[]) => void) => void | A callback function can be returned to support continuous gets (see caveats for more details) |
handleCreate | resourceName: string , data: any , options: object , meta: Meta | any | Promise<any> | |
handleUpdate | resourceName: string , query: any , data: any , options: object , meta: Meta | any | Promise<any> | |
handlePatch | resourceName: string , query: any , data: any , options: object , meta: Meta | any | Promise<any> | |
handleRemove | resourceName: string , query: any , options: object , meta: Meta | any | 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
orrecycleItems
. 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
- if one or more items differ (compared by value):
- item equality
- if an item differs (compared by value):
prevItem !== nextItem
- else (even if it was reordered in the array):
prevItem === nextItem
- if an item differs (compared by value):
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.
- Usage
- Import
const provider = new DataProvider();
if (!isDataProvider(provider)) {
throw new Error("Should be a DataProvider instance");
}
import { isDataProvider } from "@civet/core";
Function arguments
Name | Type | Description |
---|---|---|
dataProvider | any | The element to be checked |
Return type
Type | Description |
---|---|
boolean | Whether dataProvider is an instance of DataProvider |
dataProviderPropType
PropType for DataProvider
instances.
- Usage
- Import
const propTypes = {
optional: dataProviderPropType,
required: dataProviderPropType.isRequired,
};
import { dataProviderPropType } from "@civet/core";
createPlugin
Creates a plugin from the provided configuration function.
See Extending Civet for further details.
- Usage
- Import
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);
import { createPlugin } from "@civet/core";
Function arguments
Name | Type | Description |
---|---|---|
pluginDefinition | (BaseDataProvider: DataProvider) => DataProvider | A function that returns an extended version of BaseDataProvider |
Return type
Type | Description |
---|---|
(DataProvider) => DataProvider | The plugin |
compose
Composes the specified single-argument functions from right to left.
This can be especially useful when applying multiple plugins to a DataProvider.
- Usage
- Import
const DataProviderWithPlugins = compose(
pluginA,
pluginB,
pluginC
)(SomeDataProvider);
// This is the same as: pluginA(pluginB(pluginC(SomeDataProvider)))
import { compose } from "@civet/core";
Function arguments
Name | Type | Description |
---|---|---|
...fns | (a: any) => any | The functions to be composed |
Return type
Type | Description |
---|---|
(a: any) => any | The composed functions |
Notifier
Interface for handling client side notification events.
- Usage
- Import
// 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");
import { Notifier } from "@civet/core";
Class members
Name | Arguments | Return Type | Description |
---|---|---|---|
subscribe | handler: (...args: any) => void | unsubscribe: () => void | Subscribe to notifications |
isSubscribed | handler: (...args: any) => void | boolean | Whether the provided handler is currently subscribed to the notifier |
trigger | ...args: any | void | Notify all currently subscribed handlers |
ChannelNotifier
Notifier
that supports multiple separate event channels.
- Usage
- Import
// 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.
import { ChannelNotifier } from "@civet/core";
Class members
Name | Arguments | Return Type | Description |
---|---|---|---|
subscribe | handler: (channel: string, ...args: any) => void | unsubscribe: () => void | Subscribe to notifications on the specified channel |
isSubscribed | handler: (channel: string, ...args: any) => void | boolean | Whether the provided handler is currently subscribed to the specified channel |
trigger | channel: string, ...args: any | void | Notify 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.
- Usage
- Import
// 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
import { AbortSignal } from "@civet/core";
Class members
Name | Arguments | Return Type | Description |
---|---|---|---|
listen | listener: () => void | unsubscribe: () => void | Listen for abort signals |
abort | void | Abort the signal | |
lock | void | Lock the signal (The signal can no longer be aborted) | |
proxy | { listen, locked, aborted } | Creates a readonly proxy for the signal |
Class variables
Name | Type | Description |
---|---|---|
locked | boolean | Whether the signal is locked |
aborted | boolean | Whether the signal is aborted |
Meta
Meta information key value map.
- Usage
- Import
// 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);
import { Meta } from "@civet/core";
Constructor
Arguments | Description |
---|---|
base: object | All changes get applied to base if it is set |
instance: any | Instance element for the meta object |
Class members
Name | Arguments | Return Type | Description |
---|---|---|---|
clear | void | Delete all keys from the object | |
delete | key: string | any | Delete the specified key from the object - returns the deleted value |
entries | ([key: string, value: any])[] | Get all entries from the object | |
get | key: string | any | Get the value for the specified key from the object |
has | key: string | boolean | Check if the object has a value for the specified key |
keys | string[] | Get all keys from the object | |
set | key: string , value: any | void | Set the value for the specified key. Make sure that values are immutable! |
values | any[] | Get all values from the object | |
commit | prev: object | object | Get 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
Name | Type | Description |
---|---|---|
instance | any | The 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.