react-synced-object
v1.2.1
Published
A lightweight, efficient, and versatile package for seamless state synchronization across a React application.
Downloads
14
Maintainers
Readme
react-synced-object
A lightweight, efficient, and versatile package for seamless state synchronization across a React application.
Overview
- This package provides a way to create, access, and synchronize universal state across React components and classes.
- It offers a simple, user-friendly alternative to state-management libraries like redux.
- Additionally, it comes with built-in features optimized for performance.
Features
- Universal State Management: Create a
SyncedObject
with a unique key and access it from anywhere. - Automated Synchronization: Manages synchronization tasks, including automatic rerendering of dependent components, integration with third-party APIs (e.g., databases), and seamless callback handling (e.g., error handling).
- Custom Debouncing: Defers multiple consecutive data changes into a single synchronization request.
- Local Storage Functionality: Provides utility functions for finding, removing, and deleting from the browser's local storage.
Interface
SyncedObject
: A data wrapper class with a custom type, options, and methods.initializeSyncedObject
: The factory function for aSyncedObject
.useSyncedObject
: A custom React hook to interact with aSyncedObject
from any component.getSyncedObject
: A universal point of access for anySyncedObject
.deleteSyncedObject
: Safe-deletion of aSyncedObject
.updateSyncedObject
: A synchronization-aware function for modifying aSyncedObject
.findInLocalStorage
: Utility function to retrieve from local storage.removeInLocalStorage
: Utility function to delete from local storage.
Usage
- Note: Some parameters, options, and use cases are omitted for brevity - see below for the full breakdown.
Setup
npm install react-synced-object
react-synced-object
works with any version of react with the hooks[useState, useEffect, useMemo]
.
Create a Synced Object
import { initializeSyncedObject } from 'react-synced-object';
const options = { defaultValue: { myProperty: "hello world" }, debounceTime: 5000 };
const myObject = initializeSyncedObject("myObject", "local", options);
// Create a synced object with key "myObject", type "local", and the specified options.
- You can initialize a synced object from anywhere - at the root of your application, in a static class, or in a component. Just make sure to initialize it before trying to access it.
- Re-initializing a synced object simply returns the existing one - thus, you can initialize multiple times without worry.
Access a Synced Object
Access a Synced Object via getter:
import { getSyncedObject } from 'react-synced-object';
const myObject = getSyncedObject("myObject");
console.log(myObject?.data.myProperty); // "hello world"
Access a Synced Object via hook:
import { useSyncedObject } from 'react-synced-object';
function MyComponent() {
const { syncedObject } = useSyncedObject("myObject");
return (
<div>{syncedObject && syncedObject.data.myProperty}</div>
);
}
Modify a Synced Object
Modify a Synced Object via setter:
import { initializeSyncedObject } from 'react-synced-object';
const myObject = initializeSyncedObject("myObject", "local", options);
myObject.data = { myProperty: "hello world", myProperty2: "hello world again!" };
myObject.modify(0);
// Set data, then handle modifications to `myObject` with a sync debounce time of 0.
Modify a Synced Object via hook:
import { useSyncedObject } from 'react-synced-object';
function MyComponent() {
const { syncedObject } = useSyncedObject("myObject");
return (
<input onChange={(event) => {syncedObject.modify().myProperty = event.target.value}}>
</input>
);
// Set myProperty to the input's value, then handle modifications with myObject's debounce time of 5000 ms.
}
- The
modify
function immediately syncs state accross the entire application and, in this case, prepares to update local storage. - Other parameters and specific use cases are discussed below.
Details
- Note: All interactables have JSDoc annotations with helpful information, as well.
Structure
react-synced-object
uses a static classSyncedObjectManager
to manage all synced objects.- This means that synced objects will be shared across the same JavaScript environment (such as between separate tabs or windows), and will persist until page reload.
- This package was stress-tested with an upcoming full-stack application.
SyncedObject Initialization
A SyncedObject
is a wrapper class for data. Every instance must be initialized through initializeSyncedObject
with a key, type, and (optional) options.
key
: Any unique identifier, usually a string.
type
: Either "temp", "local", or "custom".
| Type | Description |
|--------|-------------------------------------------------|
| "temp" | Temporary application-wide state - won't push or pull data. |
| "local" | Persistent state - will interact with the browser's local storage. |
| "custom" | Customizable state - will call your custom sync functions. |
| | A local
or custom
synced object will attempt to pull data upon initialization, and prepare to push data upon modification.
options
: An optional object parameter with several properties.
| Property | Description |
|--------|-------------------------------------------------|
| defaultValue
= {} | The default value of SyncedObject.data
before the first sync. Serves as the initial value for temp
and local
objects. |
| debounceTime
= 0 | The period in ms to defer sync after modify
. Multiple invocations will reset this timer. |
| reloadBehavior
= "prevent" | The behavior upon attempted application unload. "prevent": Stops a page unload with a default popup if the object is pending sync. "allow": Allows a page unload even if the object has not synced yet."finish": Attempts to force sync before page unload. Warning: "finish" may not work as expected with custom sync functions, due to the nature of async functions and lack of callbacks. |
| customSyncFunctions
= { pull
: undefined, push
: undefined } | Custom synchronization callbacks invoked automatically by a custom
synced object. pull(syncedObject : SyncedObject)
: Return the requested data if successful, null
to invoke push
instead, or throw an error. push(syncedObject : SyncedObject)
: Return anything if successful, or throw an error. |
| callbackFunctions
= { onSuccess
: undefined, onError
: undefined } | Custom callbacks invoked after a local
/ custom
object's pull or push attempt. onSuccess(syncedObject : SyncedObject, status : {requestType, success, error})
onError(syncedObject : SyncedObject, status : {requestType, success, error})
The status
parameter contains properties requestType
, success
, and error
. requestType
will be "pull" or "push", success
will be true or false, and error
will be null or an Error object. |
| safeMode
= true
or false
| Whether to conduct initialization checks and warnings for this synced object. Default is true
in development, false
in production. |
SyncedObject Runtime Properties
A SyncedObject
has several runtime properties and methods which provide useful behavior.
SyncedObject.data
(property)
- This is where the information payload is stored. Access it as a normal property, change it as needed, and then call the below:
SyncedObject.modify
(method)
- This is a function to signify that a change was made to
SyncedObject
data. - It immediately updates state accross the entire application, while preparing to sync to external sources (in the case of
local
andcustom
objects). - Additionally, it returns
SyncedObject.data
, allowing us to chain property modifications with the function call itself.
| Modify | Description |
|--------|-------------------------------------------------|
| modify()
| Handle modifications with the SyncedObject
's default debounce time.
| modify(1000)
| Handle modifications, specifying the debounce time in milliseconds. This will overwrite the timers of any pending sync tasks for that SyncedObject
.
| modify("myProperty")
| Same as modify()
, but specifying the property being modified. This is helpful: For selective rerendering of useSyncedObject
components with property dependencies.To keep track of property changes when syncing a custom
object.
| modify("myProperty", 1000)
| Combining the above two calls.
SyncedObject.changelog
(property)
- This array keeps track of property names modified using
modify(propertyName)
. It is automaticaly populated and cleared upon a successful push. Accessible directly from theSyncedObject
, this property can prove useful in custom sync functions and callbacks.
SyncedObject.state
(property)
- An object with two properties:
success
anderror
, tracking the status of the last sync.
| Property | Description |
|--------|-------------------------------------------------|
|success
| Whether the last sync was successful. True
, false
, or null
if syncing.
|error
| The error of the last sync, else null
.
useSyncedObject
useSyncedObject
is an easy-to-use hook for interacting with an initializedSyncedObject
from any component.- Traditional approaches to updating component state, such as prop chaining or Context, can lead to rerendering issues, especially when dealing with complex, nested data objects. This is due to React's shallow comparison method, which often results in either unsuccessful or unnecessary rerenders.
- In contrast,
useSyncedObject
establishes direct dependencies to specificSyncedObject
properties using event listeners, resulting in highly accurate and performant component updates.
const options = {dependencies: ["modify"], properties: ["myProperty"]};
const { syncedObject, syncedData,
syncedSuccess, syncedError, modify } = useSyncedObject("myObject", options);
// This component will rerender when property `myProperty` of "myObject" is modified.
Options
- You can specify exactly when a component should rerender, through a combination of dependent events and property names.
options.dependencies
- The conditions in which component should rerender itself. Leave undefined for the default (all events), set equal to exactly one event, or set equal to an array of events.
| Dependencies | Description |
|--------|-------------------------------------------------|
|dependencies
= ["modify", "pull", "push", "error"] || undefined | The default. Will rerender on every event.
|dependencies
= [] | No dependencies. Will not rerender on any synced object event.
|dependencies
= ["modify"] | Rerenders when the synced object is modified by any source. Overrides "modify_external".
|dependencies
= ["modify_external"] | Rerenders when the synced object is externally modified, paired with modify
from useSyncedObject
. Ideal for components that are already rerendering, such as an input element.
|dependencies
= ["pull"] | Rerenders when the synced object data is pulled.
|dependencies
= ["push"] | Rerenders when the synced object data is pushed.
|dependencies
= ["error"] | Rerenders when the status.error
of the synced object changes.
options.properties
- Upon
modify
ormodify_external
, the affected properties for which a component should rerender itself. Leave undefined or set as[""]
for the default (all properties), set equal to exactly one property, or set equal to an array of properties.
| Properties | Description |
|--------|-------------------------------------------------|
|properties
= [""] || undefined | The default. Will rerender on any event regardless if the changelog has properties or not. Example: modify()
and modify("anyProperty")
.
|properties
= [] | Will only rerender if the changelog has no properties. Example: modify()
.
|properties
= ["myProperty"] | Will only rerender if the changelog includes "myProperty". Example: modify("myProperty)
.
|properties
= ["", "myProperty"] | Will rerender if the changelog includes "myProperty" or if the changelog is empty. Example: modify()
and modify("myProperty")
, Counter-Example: modify("anotherProperty)
.
- Note that in order for a rerender to occur, both the
dependencies
andproperties
must be satisfied.
options.safeMode
- Similar to the
safeMode
option frominitializeSyncedObject
- default istrue
in development,false
in production.
Return Bundle
- Most of
useSyncedObject
returns are aliases toSyncedObject
properties. However,modify
has a special use case.
| Return Value | Description |
|--------|-------------------------------------------------|
| syncedObject | Equivalent to getSyncedObject
. Either SyncedObject
or undefined.
| syncedData | Equivalent to SyncedObject?.data
.
| syncedSuccess | Equivalent to SyncedObject?.state.success
.
| syncedError | Equivalent to SyncedObject?.state.error
.
| modify | Similar to SyncedObject.modify
, if SyncedObject
exists. This version of modify
records the component that triggered the modification.This is intended to be used in tandem with the "modify_external" dependency.
Other Utility Functions
findInLocalStorage(keyPattern, returnType = "key")
const myKeys = findInLocalStorage(\myObject\);
console.log(myKeys); // ['myObject1', 'myObject2']
findInLocalStorage
will search in local storage for an exact string match or regex pattern, and return an array of keys (returnType = "key"
) or data objects (returnType = "data"
).
removeFromLocalStorage(keyPattern, affectedObjects = "ignore")
const deletedKeys = removeFromLocalStorage(\myObject\);
console.log(deletedKeys); // ['myObject1', 'myObject2']
removeFromLocalStorage
will also search in local storage, deleting any matches, and handling any affected objects.affectedObjects = ignore
: Does not delete the objects - they may re-push their data to local storage again.affectedObjects = decouple
: The affected objects will be decoupled from local storage, turning intotemp
objects.affectedObjects = delete
: The affected objects will be deleted from the manager.
deleteSyncedObject(key)
deleteSyncedObject("myObject");
deleteSyncedObject
deletes an initializedSyncedObject
with the given key from the manager. It also updates dependent components and prevents furthermodify
calls.- Be wary when deleting an
SyncedObject
initialized inside a component - it could be reinitialized.
updateSyncedObject(key, updater)
const myObject = initializeSyncedObject("myObject", "custom", {
customSyncFunctions: { pull: fetchFromDatabase } });
const myUpdatedObject = await updateSyncedObject("myObject", {
prop1: "new value", prop2: "new value2" }); // Executes once `myObject` completes initial pull.
console.log(myObject.data.prop1); // "new value"
console.log(myObject.state.success); // true
updateSyncedObject
updates the data of an initializedSyncedObject
with the given key, returning the newly modified object after attempted synchronization.- Used when the order of modification matters -
updateSyncedObject
waits until after existing sync requests finish, eliminating race conditions and ensuring access to the most recent data. - Provides the resulting state for use in error-sensitive standalone function calls.
updater
: If an object, overwrites the specified properties ofSyncedObject.data
with the provided values. If a non-object, overwrites theSyncedObject.data
field itself.
Outro
- Feel free to comment or bug report here.
- Happy Coding!