Skip to content

Utility functions and types for normalized reducers architectures

License

Notifications You must be signed in to change notification settings

lolero/normalized-reducers-utils

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

42 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

normalized-reducers-utils

Utility functions and types for normalized reducers architectures

Purpose

  1. A standard protocol to interact with an application's front-end state in order to make CRUD operations on it consistent, regardless of the data that is being stored or consumed. In simple English, this package provides the tools to interact with the state in a similar way to how clients interact with a RESTful API.

  2. A set of strongly typed CRUD action interfaces and reducer handlers that enforce a consistent reducer architecture which allows for the robust and reliable scaling of a web application.

  3. A framework that simplifies the migration of async logic away from components and into a dedicated layer of redux middleware.

The framework is designed for redux-like architectures where data is stored in reducers and interactions with the stored data happen through actions that get dispatched and hit the reducers.

Motivation

Many millions of applications with broad ranges of size, complexity and popularity are getting built today with React and Redux because they are the most popular frameworks for that purpose. Unfortunately, there is little consistency in the reducer architecture across the vast ocean of projects that include them in their stack. App state configurations and interaction protocols vary as much as or more than the apps' purposes and business logic.

Many of these projects are so busy sorting out features and aesthetics that they don't allocate much time to addressing the scalabity of their state's design, or the handling of asynchronous logic such as pending AJAX calls to remote APIs.

As the amount and complexity of the app's features grow, their maintenance and scaling becomes increasingly more expensive and cumbersome. This is a result of a growing amount of reducers with a variety of structures for their respective purposes, as well as the non standardized handling of an increasing number of async calls to RESTful servers for a variety of data that the app requires.

This project started while refactoring an app that consumes many related entities from their respective microservices and presents their data in a web interface. As more entities were added to the application, maintaining the reducers that stored their data and the actions that modified them became very inefficient. What's more, the scattered promises that handled the fetching of the entities, sometimes in component's methods and others in redux middleware or custom service classes, made the management and tracking of the ongoing calls difficult to follow and debug. Then, after cleaning up the state and migrating all async logic to a dedicated layer in redux middleware, the benefits of a standard reducer structure and reducer-hitting actions became evident and this set of generic types and functions began to come together as an underlying architecture for scalable react-redux applications.

A standard structure for all reducers and the interactions with them simplifies their maintenance. On the other hand, the normalization of reducers that store entity data simplifies the access-to and control-of both the entities and the relationships between them. Naturally, this results in the ability to better scale the state, regardless of the type of data that is stored and managed in it; be it data entities stored in a database, metadata related to the presentational state of a component in the scope of a session, or any other type of data for any use whatsoever.

Getting started

Install

$ npm install --save normalized-reducers-utils

or

yarn add normalized-reducers-utils

Configuration

The initial state of a reducer is created by calling the createInitialState function which takes an optional config object with the following props.

successRequestsCache

  • Type: number | null
  • Default: 10

Number of successfully completed requests to keep in the reducer's requests prop

Set to null to keep all successfully completed requests.

failRequestsCache

  • Type: number | null
  • Default: null

Number of failed requests to keep in the reducer's requests prop

Set to null to keep all failed requests.

requestsPrettyTimestamps

  • Type (optional):
{
  format: string;
  timezone: string;
}
  • Default: undefined

Format of requests' formatted string timestamps.

Basic concepts

Normalized reducers

Normalized reducers are reducers that have the standard reducer object structure.

This structure is deliberately designed for reducers to store two kinds of data.

Reducers can contain both types of data or only one of the two.

1. Reducer metadata

Metadata about the reducer itself. This can be data related to the state of the reducer, regarding the collection of entity data stored in the reducer, or any other information not related to any particular single data entity.

2. Entity data

Entity data, e.g. records from a back-end database table / collection

Reducer props

requests

Requests corresponding to request actions to modify a reducer.

Request actions are actions with the '__REQUEST' suffix in the action type.

When a request action gets dispatched, a request object is added to the requests prop of the reducer. Requests are indexed by the requestId included in the request action and contain the following props about the requested CRUD operation that should be performed on the reducer.

metadata

The reducer's metadata is a standard object with string keys.

data

The reducer's entity data is indexed by the entities's primary key (PK).

An entity's PK is a concatenated string of the entity's props and/or its __edges__, as defined in the reducer's PK schema.

config

The reducer configuration object with all config params.

Reducer actions

As stated above, normalized reducers are often used to store entity data, usually fetched asynchronously from remote RESTful APIs. This is why the reducer contains a requests prop and all interactions with normalized reducers should be initiated with a request action and completed with a success or fail action.

API

Actions utils

wasRequestSuccessful

function* wasRequestSuccessful(requestAction: {
  type: string;
  requestId: string;
}): boolean;

An effect to be placed in a saga that takes a request action and waits for it to be completed

Example:

function* fetchUser1AndThenUser2(): Generator<
  CallEffect | PutEffect,
  void,
  boolean
> {
  const getUser1RequestAction = usersCreateActionGetOneRequest('user-1');
  yield put(getUser1Action);

  const wasGetUser1ActionSuccessful = (yield call(
    wasRequestSuccessful,
    getUser1RequestAction,
  )) as boolean;
  if (!wasGetUser1ActionSuccessful) {
    console.error('failed to get user 1');
  }

  const getUser2RequestAction = usersCreateActionGetOneRequest('user-2');
  yield put(getUser2RequestAction);
}

Initial state utils

createInitialState

function createInitialState<
  ReducerMetadataT extends ReducerMetadata,
  EntityT extends Entity,
>(
  initialReducerMetadata: ReducerMetadataT,
  initialReducerData: ReducerData<EntityT>,
  config?: Partial<ReducerConfig>,
): Reducer<ReducerMetadataT, EntityT>;

Creates a typed initial state for a reducer.

** The EntityT generic type required by this function is not inferred from the function's arguments when initialReducerData is an empty object, therefore it is recommended that consumers declare the function's generic types explicitly in function calls.

Normalizer utils

normalizeEntityArrayByPk

function normalizeEntityArrayByPk<
  EntityT extends Entity,
  PkSchemaT extends PkSchema<
    EntityT,
    PkSchemaFields<EntityT>,
    PkSchemaEdges<EntityT>
  >,
>(pkSchema: PkSchemaT, entityArray: EntityT[]): ReducerData<EntityT>;

Converts an array of entities into an object, indexed by the entities' PKs.

PK utils

createReducerPkUtils

function createReducerPkUtils<
  ReducerMetadataT extends ReducerMetadata,
  EntityT extends Entity,
  PkSchemaT extends PkSchema<
    EntityT,
    PkSchemaFields<EntityT>,
    PkSchemaEdges<EntityT>
  >,
>(pkSchema: PkSchemaT): ReducerPkUtils<ReducerMetadataT, EntityT, PkSchemaT>;

Creates an object that contains a reducer's Pk schema as well as PK utility functions.

pkSchema

The PK schema used to create the PK concatenated strings that index the entities in the reducer's data prop.

getPkOfEntity

A function that takes an entity and returns the entity's PK

destructPk

A function that takes an entity's PK and returns a destructed entity PK

emptyPkSchema

const emptyPkSchema: PkSchema<Entity, [], []> = {
  fields: [],
  edges: [],
  separator: '',
  subSeparator: '',
};

An empty PK schema to initialize reducers that don't store entity data.

Reducer handlers

An abstraction layer of utility functions that handle the manipulation of the reducer's state for CRUD operations on reducer's metadata or on entity data.

Example:

function UsersReducer(
  state: UsersReducer = usersInitialState,
  action: UsersReducerHittingAction,
): UsersReducer {
  switch (action.type) {
    case UsersActionTypes.USERS_GET_MANY__REQUEST:
      return handleRequest(state, action);
    case UsersActionTypes.USERS_GET_MANY__SUCCESS:
      return handleSaveWholeEntities(state, action);
    case UsersActionTypes.USERS_GET_MANY__FAIL:
      return handleFail(state, action);
    default:
      return state;
  }
}

handleDeleteEntities

function handleDeleteEntities<
  ActionTypeT extends string,
  ReducerMetadataT extends ReducerMetadata,
  EntityT extends Entity,
>(
  state: Reducer<ReducerMetadataT, EntityT>,
  action: DeleteEntitiesAction<ActionTypeT, ReducerMetadataT>,
): Reducer<ReducerMetadataT, EntityT>;

handleFail

function handleFail<
  ActionTypeT extends string,
  ReducerMetadataT extends ReducerMetadata,
  EntityT extends Entity,
>(
  state: Reducer<ReducerMetadataT, EntityT>,
  action: FailAction<ActionTypeT>,
): Reducer<ReducerMetadataT, EntityT>;

handleRequest

function handleRequest<
  ActionTypeT extends string,
  ReducerMetadataT extends ReducerMetadata,
  EntityT extends Entity,
  RequestMetadataT extends RequestMetadata,
>(
  state: Reducer<ReducerMetadataT, EntityT>,
  action: RequestAction<ActionTypeT, RequestMetadataT>,
): Reducer<ReducerMetadataT, EntityT>;

handleSavePartialEntities

function handleSavePartialEntities<
  ActionTypeT extends string,
  ReducerMetadataT extends ReducerMetadata,
  EntityT extends Entity,
>(
  state: Reducer<ReducerMetadataT, EntityT>,
  action: SavePartialEntitiesAction<ActionTypeT, ReducerMetadataT, EntityT>,
): Reducer<ReducerMetadataT, EntityT>;

handleSavePartialPatternToEntities

function handleSavePartialPatternToEntities<
  ActionTypeT extends string,
  ReducerMetadataT extends ReducerMetadata,
  EntityT extends Entity,
>(
  state: Reducer<ReducerMetadataT, EntityT>,
  action: SavePartialPatternToEntitiesAction<
    ActionTypeT,
    ReducerMetadataT,
    EntityT
  >,
): Reducer<ReducerMetadataT, EntityT>;

handleSaveNothing

function handleSaveNothing<
  ActionTypeT extends string,
  ReducerMetadataT extends ReducerMetadata,
  EntityT extends Entity,
>(
  state: Reducer<ReducerMetadataT, EntityT>,
  action: SaveNothingAction<ActionTypeT>,
): Reducer<ReducerMetadataT, EntityT>;

handleSaveWholeReducerMetadata

function handleSaveWholeReducerMetadata<
  ActionTypeT extends string,
  ReducerMetadataT extends ReducerMetadata,
  EntityT extends Entity,
>(
  state: Reducer<ReducerMetadataT, EntityT>,
  action: SaveWholeReducerMetadataAction<ActionTypeT, ReducerMetadataT>,
): Reducer<ReducerMetadataT, EntityT>;

handleSavePartialReducerMetadata

function handleSavePartialReducerMetadata<
  ActionTypeT extends string,
  ReducerMetadataT extends ReducerMetadata,
  EntityT extends Entity,
>(
  state: Reducer<ReducerMetadataT, EntityT>,
  action: SavePartialReducerMetadataAction<ActionTypeT, ReducerMetadataT>,
): Reducer<ReducerMetadataT, EntityT>;

handleSaveWholeEntities

function handleSaveWholeEntities<
  ActionTypeT extends string,
  ReducerMetadataT extends ReducerMetadata,
  EntityT extends Entity,
>(
  state: Reducer<ReducerMetadataT, EntityT>,
  action: SaveWholeEntitiesAction<ActionTypeT, ReducerMetadataT, EntityT>,
): Reducer<ReducerMetadataT, EntityT>;

Selectors creators

createReducerSelectors

function createReducerSelectors<
  ReducerMetadataT extends ReducerMetadata,
  EntityT extends Entity,
  ReducerPathT extends string[],
  ReduxState extends ReducerGroup<ReducerMetadataT, EntityT, ReducerPathT>,
>(
  reducerPath: ReducerPathT,
): ReducerSelectors<ReducerMetadataT, EntityT, ReduxState>;

Creates an object that contains reselect selectors to select a reducer's props.

Hooks creators

createReducerSelectors

function createReducerHooks<
  ReducerMetadataT extends ReducerMetadata,
  EntityT extends Entity,
  ReducerPathT extends string[],
  ReduxState extends ReducerGroup<ReducerMetadataT, EntityT, ReducerPathT>,
>(
  reducerSelectors: ReducerSelectors<
    ReducerMetadataT,
    EntityT,
    ReducerPathT,
    ReduxState
  >,
): ReducerHooks<ReducerMetadataT, EntityT>;

Creates an object that contains React hooks to retrieve a reducer's props, as well as individual requests and entities.

Types

Action types

Request actions

RequestAction
type RequestAction<
  ActionTypeT extends string,
  RequestMetadataT extends RequestMetadata,
> = {
  type: ActionTypeT;
  requestMetadata: RequestMetadataT;
  requestId: string;
};

Success actions

DeleteEntitiesAction
type DeleteEntitiesAction<
  ActionTypeT extends string,
  ReducerMetadataT extends ReducerMetadata,
> = {
  type: ActionTypeT;
  entityPks: string[];
  partialReducerMetadata?: Partial<ReducerMetadataT>;
  requestId?: string;
  subRequests?: SubRequest[];
  statusCode?: number;
};
SavePartialEntitiesAction
type SavePartialEntitiesAction<
  ActionTypeT extends string,
  ReducerMetadataT extends ReducerMetadata,
  EntityT extends Entity,
> = {
  type: ActionTypeT;
  partialEntities: ReducerPartialData<EntityT>;
  partialReducerMetadata?: Partial<ReducerMetadataT>;
  requestId?: string;
  subRequests?: SubRequest[];
  statusCode?: number;
};
SavePartialPatternToEntitiesAction
type SavePartialPatternToEntitiesAction<
  ActionTypeT extends string,
  ReducerMetadataT extends ReducerMetadata,
  EntityT extends Entity,
> = {
  type: ActionTypeT;
  entityPks: string[];
  partialEntity: Partial<
    Omit<EntityT, '__edges__'> & {
      __edges__?: Partial<EntityT['__edges__']>;
    }
  >;
  partialReducerMetadata?: Partial<ReducerMetadataT>;
  requestId?: string;
  subRequests?: SubRequest[];
  statusCode?: number;
};
SavePartialReducerMetadataAction
type SavePartialReducerMetadataAction<
  ActionTypeT extends string,
  ReducerMetadataT extends ReducerMetadata,
> = {
  type: ActionTypeT;
  partialReducerMetadata: Partial<ReducerMetadataT>;
  requestId?: string;
  subRequests?: SubRequest[];
  statusCode?: number;
};
SaveWholeEntitiesAction
type SaveWholeEntitiesAction<
  ActionTypeT extends string,
  ReducerMetadataT extends ReducerMetadata,
  EntityT extends Entity,
> = {
  type: ActionTypeT;
  wholeEntities: ReducerData<EntityT>;
  partialReducerMetadata?: Partial<ReducerMetadataT>;
  requestId?: string;
  subRequests?: SubRequest[];
  statusCode?: number;
  flush?: boolean;
};

Fail actions

FailAction
type FailAction<ActionTypeT extends string> = {
  type: ActionTypeT;
  error: string;
  requestId: string;
  statusCode?: number;
};

PK types

DestructedPk

type DestructedPk<
  EntityT extends Entity,
  PkSchemaT extends PkSchema<
    EntityT,
    PkSchemaFields<EntityT>,
    PkSchemaEdges<EntityT>
  >,
> = {
  fields: { [field in PkSchemaT['fields'][number]]: string };
  edges: { [edge in PkSchemaT['edges'][number]]: string[] };
};

PkSchema

type PkSchema<
  EntityT extends Entity,
  FieldsT extends PkSchemaFields<EntityT>,
  EdgesT extends PkSchemaEdges<EntityT>,
> = {
  fields: FieldsT;
  edges: EdgesT;
  separator: string;
  subSeparator: string;
};

PkSchemaEdges

type PkSchemaEdges<EntityT extends Entity> = (keyof EntityT['__edges__'])[];

PkSchemaFields

type PkSchemaFields<EntityT extends Entity> = Exclude<
  keyof EntityT,
  '__edges__'
>[];

ReducerPkUtils

type ReducerPkUtils<
  ReducerMetadataT extends ReducerMetadata,
  EntityT extends Entity,
  PkSchemaT extends PkSchema<
    EntityT,
    PkSchemaFields<EntityT>,
    PkSchemaEdges<EntityT>
  >,
> = {
  pkSchema: PkSchemaT;
  getPkOfEntity: (entity: EntityT) => string;
  destructPk: (pk: string) => DestructedPk<EntityT, PkSchemaT>;
};

Reducer types

Entity

type Entity<ReducerEdgesT extends ReducerEdges> = {
  [fieldKey: string]: unknown;
  __edges__?: {
    [edgeName in keyof ReducerEdgesT]: string[] | null;
  };
};

Reducer

type Reducer<
  ReducerMetadataT extends ReducerMetadata,
  EntityT extends Entity,
> = {
  requests: { [requestId: string]: Request };
  metadata: ReducerMetadataT;
  data: ReducerData<EntityT>;
  config: ReducerConfig;
};

ReducerConfig

type ReducerConfig = {
  successRequestsCache: number | null;
  failRequestsCache: number | null;
  requestsPrettyTimestamps?: {
    format: string;
    timezone: string;
  };
};

ReducerData

type ReducerData<EntityT extends Entity> = {
  [entityPk: string]: EntityT;
};

ReducerEdge

type ReducerEdge = {
  nodeReducerPath: string[];
  edgeReducerPath?: string[];
  edgeSide?: EdgeSide;
};

ReducerEdges

type ReducerEdges = {
  [edgeName: string]: ReducerEdge;
};

ReducerGroup

type ReducerGroup<
  ReducerMetadataT extends ReducerMetadata,
  EntityT extends Entity,
  ReducerPathT extends string[],
> = {
  [reducerOrGroup in ReducerPathT[number]]?:
    | Reducer<ReducerMetadataT, EntityT>
    | ReducerGroup<ReducerMetadataT, EntityT, ReducerPathT>;
};

ReducerMetadata

type ReducerMetadata = {
  [metadataKey: string]: unknown;
};

ReducerPartialData

type ReducerPartialData<EntityT extends Entity> = {
  [entityPk: string]: Partial<
    Omit<EntityT, '__edges__'> & {
      __edges__?: Partial<EntityT['__edges__']>;
    }
  >;
};

EdgeSide

enum EdgeSide {
  slave,
  master,
}

Request

type Request = {
  id: string;
  createdAt: {
    unixMilliseconds: number;
    formattedString?: string;
  };
  completedAt?: {
    unixMilliseconds: number;
    formattedString?: string;
  };
  isPending: boolean;
  metadata: RequestMetadata;
  isOk?: boolean;
  entityPks?: string[];
  statusCode?: number;
  error?: string;
  subRequests?: SubRequest[];
};

RequestMetadata

type RequestMetadata = {
  [requestMetadataKey: string]: unknown;
};

SubRequest

type SubRequest = {
  reducerName: string;
  requestId: string;
};

Selector types

ReducerSelectors

type ReducerSelectors<
  ReducerMetadataT extends ReducerMetadata,
  EntityT extends Entity,
  ReducerPathT extends string[],
  ReduxState extends ReducerGroup<ReducerMetadataT, EntityT, ReducerPathT>,
> = {
  selectRequests: OutputSelector<
    ReduxState,
    Reducer<ReducerMetadataT, EntityT>['requests'],
    (
      res: Reducer<ReducerMetadataT, EntityT>,
    ) => Reducer<ReducerMetadataT, EntityT>['requests']
  >;
  selectMetadata: OutputSelector<
    ReduxState,
    Reducer<ReducerMetadataT, EntityT>['metadata'],
    (
      res: Reducer<ReducerMetadataT, EntityT>,
    ) => Reducer<ReducerMetadataT, EntityT>['metadata']
  >;
  selectData: OutputSelector<
    ReduxState,
    Reducer<ReducerMetadataT, EntityT>['data'],
    (
      res: Reducer<ReducerMetadataT, EntityT>,
    ) => Reducer<ReducerMetadataT, EntityT>['data']
  >;
  selectConfig: OutputSelector<
    ReduxState,
    Reducer<ReducerMetadataT, EntityT>['config'],
    (
      res: Reducer<ReducerMetadataT, EntityT>,
    ) => Reducer<ReducerMetadataT, EntityT>['config']
  >;
};

Hook types

ReducerHooks

type ReducerHooks<
  ReducerMetadataT extends ReducerMetadata,
  EntityT extends Entity,
> = {
  useRequest: (
    requestId: string,
  ) => Reducer<ReducerMetadataT, EntityT>['requests'][string] | undefined;
  useRequests: (
    requestIds?: string[],
  ) => Partial<Reducer<ReducerMetadataT, EntityT>['requests']>;
  useReducerMetadata: () => Reducer<ReducerMetadataT, EntityT>['metadata'];
  useEntity: (
    entityPk: string,
  ) => Reducer<ReducerMetadataT, EntityT>['data'][string] | undefined;
  useEntities: (
    entityPks?: string[],
  ) => Partial<Reducer<ReducerMetadataT, EntityT>['data']>;
  useReducerConfig: () => Reducer<ReducerMetadataT, EntityT>['config'];
};

Donate

I developed this framework entirely in my free time and without monetary retribution. You are welcome and encouraged to use it free of charge but if it serves your purpose and you want to contribute to the project, any amount of donation is greatly appreciated!

Paypal BTC ETH
https://paypal.me/pools/c/8t2WvAATaG bc1q7gq4crnt2t47nk9fnzc8vh488ekmns7l8ufj7z 0x220E622eBF471F9b12203DC8E2107b5be1171AA8

Acknowledgments

Thanks to AngSin for his valuable contributions.

About

Utility functions and types for normalized reducers architectures

Resources

License

Stars

Watchers

Forks

Packages

No packages published