@rlean/core
Advanced tools
Weekly downloads
Readme
The purpose of this package is to remove the boilerplate code that becomes unruly when working in enterprise level React applications. This package handles the state, storage, middleware, API calls, and suggests structure and implementation in the Web app. There is no need for smart components or dumb components, only functional components. All global state objects have entities. Entities are configured with properties that tells this package how to handle the behavior of that object, and the state for that object can be managed by invoking any of the package's custom hooks and functions: useGet, usePost, usePut, usePatch, useDelete, useSave, useRemove, and removeAll.
To install in an existing project, follow these steps:
npm i @rlean/core --save
Create an entities folder somewhere in your app. Make sure this folder contain an index.js file to export all entities.
Add a configuration file that will be used when initializing the @rlean/core package.
Example configuration:
Note: logToConsole is set to true for the example. A better approach would be to set it to something like
logToConsole: process.env.REACT_APP_ENV !== 'production'
so your global state is not visible to your end users in the console.
Also, getToken() needs to handle token refreshes for your application.
import * as entities from 'lib/entities';
import { getToken } from 'config';
export const rLean = {
entities,
api: {
headers: {
Authorization: `Bearer ${getToken()}`,
},
uri: process.env.REACT_APP_API_URI,
},
logToConsole: true,
};
In the index.js file at the root of the project, include the following imports:
import { RLean, StateProvider } from '@rlean/core';
import { rLean as config } from 'config';
Initialize the @rlean/core package:
RLean.init(config);
And wrap the App component in the StateProvider:
ReactDom.render(
<StateProvider>
<App />
</StateProvider>
);
If you are using typescript, the StateProvider should be typed like this:
ReactDom.render(
<StateProvider<typeof config.entities>>
<App />
</StateProvider>
);
That's it! Now you can start using @rlean/core functions within the project. If you want to take advantage of the benefits of typescript, checkout the hooks section and the definition of entities in this guide.
This framework uses Axios for API calls and localForage for storage by default. These can be overridden by including your own custom adapters in lib/adapters and including these in your configuration file:
import * as entities from 'lib/entities';
import { ApiAdapter, StorageAdapter } from 'lib/adapters';
import { getToken } from 'config';
export const rLean = {
entities,
api: {
headers: {
Authorization: `Bearer ${getToken()}`,
},
uri: process.env.REACT_APP_API_URI,
adapter: ApiAdapter,
},
storage: {
adapter: StorageAdapter,
},
logToConsole: true,
};
An API adapter should have the following structure. Any unnecessary methods can be omitted.
import { ApiAdapter as ApiInterface } from '@rlean/core';
class ApiAdapter extends ApiInterface {
async get(apiPayload) {
const { url } = apiPayload;
// fetch data
// return response
}
async post(apiPayload) {
const { url, data } = apiPayload;
// fetch data
// return response
}
async put(apiPayload) {
const { url, data } = apiPayload;
// fetch data
// return response
}
async patch(apiPayload) {
const { url, data } = apiPayload;
// fetch data
// return response
}
async del(apiPayload) {
const { url, data } = apiPayload;
// fetch data
// return response
}
}
export default new AxiosAdapter();
A storage adapter should have the following structure. All functions are required.
class StorageAdapter {
async set(key, value) {
if (!key || value === undefined) {
throw new Error('Key or value cannot be undefined');
}
// setItem
}
async get(key) {
if (!key) throw new Error('Must supply a key in get');
// return getItem
}
async clear() {
// clear
}
async remove(key) {
if (!key) throw new Error('Must supply a key in remove');
// removeItem
}
}
export default new StorageAdapter();
This is an example of an entity that doesn't get populated from an API call. initialState, types, reducer, and updateState can be omitted and the following will be autogenerated by rlean/core.
import { define } from '@rlean/core';
export type EntityType = {
id: string;
attr: string;
};
const Entity = define<EntityType>('entityName');
To populate this model from an API call, include the following in options:
const Entity = define<EntityType>('entityName', {
getURL: '/apiPath',
});
If this API call includes path params, set the getURL accordingly.
const DemoEntity = define('demoEntity', {
getURL: '/SomeApiPath/:id',
});
If query string params will be used, don't include them in the attribute. The query string params will be built out for you based on what is provided as params when using the useGet custom hook.
The define function has two required parameters, the key of the entity and its options. This is their definitions:
type EntityDefineOptions<T> = {
key: string;
initialState?: Partial<T>;
getURL?: string;
postURL?: string;
putURL?: string;
patchURL?: string;
deleteURL?: string;
nullableParams?: boolean;
persistData?: boolean;
preferStore?: boolean;
progressiveLoading?: boolean;
syncInterval?: number;
syncAfterTimeElapsed?: boolean;
apiUriOverride?: string;
adapters?: Adapter;
queueOffline?: boolean;
type?: string;
updateState?: Function;
reducer?: Function;
includeInState?: boolean;
listener?: Function;
extensions?: any;
};
const define = <T>(
key: string,
options: Partial<EntityDefineOptions<T>>,
callback?: Function
): EntityDefineOptions<T>;
All attributes are optional, but some are needed to get some functionalities. Specifically, if the entity is to be connected to an API, the URLs options are needed. See bellow for more details.
The following props can be added to customize your entity's behavior.
const DemoEntity = define('demoEntity', {
initialState: {},
});
postURL
is the path that will be used when the entity instance is passed in post.
const DemoEntity = define('demoEntity', {
postURL: '/SomeApiPath',
});
putURL
is the path that will be used when the entity instance is passed in put.
const DemoEntity = define('demoEntity', {
putURL: '/SomeApiPath',
});
deleteURL
is the path that will be used when the entity instance is passed in del.
const DemoEntity = define('demoEntity', {
deleteURL: '/SomeApiPath',
});
patchURL
is the path that will be used when the entity instance is passed in patch.
const DemoEntity = define('demoEntity', {
patchURL: '/SomeApiPath',
});
nullableParams
is false by default. If an optional param is not needed by the web app, simply omit it. the purpose of this attribute is to prevent unnecessary calls to the API before the param objects have been initialized. This is available as an override in case null is a valid value for a param. This cannot be set for individual params, but rather at the entity level.
const DemoEntity = define('demoEntity', {
getURL: '/SomeApiPath',
postURL: '/SomeApiPath',
nullableParams: true,
});
If persistData
is false, data isn't stored to storage. Api is called every time. This will override preferStore
(because there's no stored value). This is true by default.
const DemoEntity = define('demoEntity', {
getURL: '/SomeApiPath',
persistData: false,
});
If preferStore
is true, it will rely on storage instead of calling the API repeatedly. This will override progressiveLoading
.
const DemoEntity = define('demoEntity', {
getURL: '/SomeApiPath',
preferStore: true,
});
These are the default functions if they are omitted. The updateState function is your action. This is what will be called to update your object in state. Type is not needed if there is only one type in your entity.
const DemoEntity = define('demoEntity', {
getURL: '/SomeApiPath',
reducer: (state, action) => {
switch (action.type) {
case 'SET_DEMO_ENTITY:
return {
...state,
...action.demoEntity
};
default:
return state;
}
},
updateState: (demoEntity, type) {
return {
type,
demoEntity
}
}
});
If using the optional type to update a part of the object in state instead of the entire object, just use a switch statement in updateState like in the reducer, and pass the type as a parameter in useGet, save, remove, post, put, patch, and del.
Use the useGlobalState custom hook to access global state.
import { useGlobalState } from '@rlean/core';
import * as entities from 'lib/entities';
const [{ stateObject, anotherStateObject }] = useGlobalState<typeof entities>();
Typing the useGlobalState using the typeof entities, we will be able to autocomplete all state objects. These objects will have the type of EntityState<EntityType>
.
The useGet custom hook is what populates all of your state objects based on whatever properties are set in your entity, and can be called from any component that relies on that state object. A dependency will be created for the param values, so if the params change, the custom hook will fire again. If no params are set, the custom hook will fire only once. useGet also takes an optional callback param that will be provided with the state value set in the custom hook, as well as the response if an API call is made. Note that the component is wrapped in React Memo, as all components using state values should be. This package uses Context API under the hood and this will prevent components from re-rendering unnecessarily.
Note: this also relies on @rlean/utils to check that ID of someStateValue exists before attempting to use the value. This approach also assumes that demoEntity cannot be null, and that the initial state value is null, but an empty value from the API is a valid value.
import React, { memo } from 'react';
import { useGlobalState, useGet } from '@rlean/core';
import { getValue } from '@rlean/utils';
import { Spinner } from 'some-ui-library';
import { DemoEntity } from 'lib/entities';
export const MyReactComponent = memo(() => {
const [{ demoEntity, someStateValue, isLoading }] = useGlobalState();
const id = getValue(someStateValue, 'id', null);
useGet({ entity: DemoEntity, params: { id } });
if (!demoEntity || demoEntity.isLoading) {
return <Spinner />
}
return (
// some component dependent on demoEntity
)
});
An example of useGet using the optional callback:
useGet(
{
entity: DemoEntity,
params: {
id: id,
},
},
(value, response) => {
if (response.status !== 200) {
// handle error
}
if (value) {
// Do something with the value. Note that storage is handled for you and the value should be accessed using the getGlobalState hook if possible.
}
}
);
It's also possible to use the useGet hook in this way:
import React, { memo } from 'react';
import { useGlobalState, useGet } from '@rlean/core';
import { getValue } from '@rlean/utils';
import { Spinner } from 'some-ui-library';
import { DemoEntity } from 'lib/entities';
export const MyReactComponent = memo(() => {
const [{ demoEntity, someStateValue }] = useGlobalState();
const [get] = useGet();
const id = getValue(someStateValue, 'id', null);
if (id) {
get({ entity: DemoEntity, params: { id } });
}
if (!demoEntity || demoEntity.isLoading) {
return <Spinner />
}
return (
// some component dependent on demoEntity
)
});
If the getURL attribute looks like this and the value of id is 1:
const DemoEntity = define('demoEntity', {
getURL: '/SomeApiPath/:id',
});
The call will look like: (uri-from-config)/SomeApiPath/1
If the getPath looks like this and the value of id is 1:
const DemoEntity = define('demoEntity', {
getURL: '/SomeApiPath',
});
The call will look like: (uri-from-config)/SomeApiPath?id=1
The usePost hook is used to post against the API and takes an options object and an optional callback function.
import { useGlobalState, usePost } from '@rlean/core';
import { DemoEntity } from 'lib/entities';
const [post] = usePost();
const updateDb = async () => {
await post({ entity: DemoEntity, body: { value: 'value' } });
};
If the entity has been typed, the body will expect the entity's type. Using the callback function, the response
will be typed as APIResponse<unknown>
, but it can be typed by manually typing the post function. This gives more flexibility in the Request and Response typing.
import { useGlobalState, usePost } from '@rlean/core';
import { DemoEntity } from 'lib/entities';
const [post] = usePost();
// response typed as `APIResponse<unknown>`
const updateDb = async () => {
await post(
{
entity: DemoEntity,
body: {
value: 'value',
},
},
response => {
if (response) {
// handle response
}
}
);
};
// response typed correctly
const updateDb = async () => {
await post<
DemoEntityType, // The Response type
Omit<DemoEntityType, 'id'>, // The Request type, `body`
typeof DemoEntity // The Expected entity type
>(
{
entity: DemoEntity,
body: {
value: 'value',
},
},
response => {
if (response) {
// handle response
}
}
);
};
The usePatch, usePut, and useDelete hooks work similarly to the usePost hook and have the same syntax. The typings works similarly as well in these function.
The options that are available for use with useGet are entity and params. The options that are available for usePost, usePatch, usePut, and useDelete are entity, body, and save. The options available for useSave are entity and value. The save option is false by default. If set to true, the response data will override the state object and store object if persistData is set to true on the entity.
The useSave hook is used when saving a state value, and takes an options object that includes the entity being updated and the new value, and an optional type. Saving a value will update state and storage if the persistData attribute is 'true' on the entity (the default setting).
import { useGlobalState, useSave } from '@rlean/core';
import { DemoEntity } from 'lib/entities';
const [save] = useSave();
const buttonClicked = async newValue => {
await save({ entity: DemoEntity, value: newValue });
};
The useRemove hook is used to remove an object from state and storage if applicable, and takes an options object that includes the entity being updated.
import { useGlobalState, useRemove } from '@rlean/core';
import { DemoEntity } from 'lib/entities';
const [remove] = useRemove();
const removeValue = async () => {
await remove({ entity: DemoEntity });
};
The removeAll function is an asynchronous function that is used to clear all storage data.
isLoading is a property that is included by default on the entity if the state object is populated by an API call. This can be used to render loading animations.
import { useGlobalState } from '@rlean/core';
import { Spinner } from 'some-ui-library';
import { DemoEntity } from 'lib/entities';
export const MyReactComponent = () => {
const [{ demoEntity }] = useGlobalState();
if (demoEntity.isLoading) {
return <Spinner />;
}
return {
/* component dependent on demoEntity */
};
};
The init property will be set to true once the entity has been initialized by the framework.
The data property will be set to the value returned from the API if the response is an array.
lastUpdated is a property that is included by default on the entity if the state object is populated by an API call. This is useful for debugging.
Calling the refetch function on an entity will cause the get function to execute again without setting the isLoading property. This is useful for loading data in the background after the initial call has already been made.
import React from 'react';
import { useGlobalState, useGet } from '@rlean/core';
import { Spinner } from 'some-ui-library';
import { DemoEntity } from 'lib/entities';
export const MyReactComponent = () => {
const [{ demoEntity }] = useGlobalState();
const id = 1;
useGet({ entity: DemoEntity, params: { id } });
setInterval(function () {
if (demoEntity.lastUpdated) {
demoEntity.refetch();
}
}, 500000);
if (demoEntity.isLoading) {
return <Spinner />;
}
return {
/* component dependent on demoEntity */
};
};
The isRefetching property works similarly to the isLoading property, but is used when the refetch function is called.
LastUpdated is a model that is include by default if there are entities that make calls against the API to populate one or more objects in state. This state object is used by the syncAfterTimeElapsed model attribute, but is also useful for debugging.
Wrap your functional components in React memo. This package uses Context API for state management. Using React memo will prevent your components from re-rendering unnecessarily when there are state changes that your components don't care about.
Make sure entities are included in the export files in the lib/entities folder. If they are not all exported from the index.js file, those objects will not work.
Make sure entities and utilities are included in the export files in the lib/entities and lib/utilities folder. If they are not all exported from the index.js files in each of those folders, those objects will not work.
FAQs
Lean React Enterprise Framework with Hooks
The npm package @rlean/core receives a total of 27 weekly downloads. As such, @rlean/core popularity was classified as not popular.
We found that @rlean/core demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket installs a Github app to automatically flag issues on every pull request and report the health of your dependencies. Find out what is inside your node modules and prevent malicious activity before you update the dependencies.