• Stars
    star
    187
  • Rank 202,521 (Top 5 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created almost 8 years ago
  • Updated over 7 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

WIP. An abstraction layer around handling normalized entity storage and data fetching with redux

redux-entity

This library is intended to make building large apps with redux easier.

Applications often evolve from relatively simple requirements (ie, grab data from a server and display it) to more complicated requirements such as loading states, optimistic updates, offline caching, cache invalidation, complex validation, etc.

Such requirements carry with them a lot of nuance and corner cases that aren't always dealt with (especially if it's not a critical feature). Moreover, the complexity of these requirements often lead to the code being much harder to understand and has a lot of boilerplate that makes the core functionality of the code harder to understand.

My hope is that this library can act as a layer of abstraction around some of these issues, resulting in much cleaner and easier to understand code without circumventing amy of the core principles around redux.

You can think of this library as one giant immutable data structure that was built specifically for the needs of common front-end application scenarios.

Features

I'm planning on adding a few things to this library to start, and see where needs develop in an actual application.

The features currently planned are:

  • Normalized Data Storage (ie, one source of truth)
  • Optimistic Updates
  • Asynchronous Validation + Form State
  • Offline first strategies (persisting state)
  • System for making user-land plugins
  • Add propType validators to Schema objects
  • Allow Schema to specify base type: Object | Map | Record
  • Move visiting logic to the Schema base class
  • Take advantage of reselect
  • Use graphql-anywhere to read from the state atom
  • Basic Resource plugins
    • SimpleList
    • PagedList
    • FilteredList
    • FilteredPagedList

Usage

You will want to start your app off by defining the "schema" of your app. Use Schema to create all of the different entities you care about in your frontend. After creating them, you will want to define their relationships with one another. The relationships of these schema is inspired from the normalizr library.

// appSchema.js

import {
  Schema,
  arrayOf,
  PagedFilteredList,
} from 'redux-entity';

export const User = new Schema('users');
export const Listing = new Schema('listings');
export const Reservation = new Schema('reservations');
export const SearchResults = new PagedFilteredList('searchResults', Listing);

Listing.define({
  host: User,
});

User.define({
  listings: arrayOf(Listing),
  reservations: arrayOf(Reservation),
});

Reservation.define({
  host: User,
  guest: User,
  listing: Listing,
});

You should do all of the normal things you would do to set up redux. When you do, you should create an entities reducer. (The name doesn't matter, but will be the location of the state atom that holds all of the state related to your server-backed data.

The state controlled by your entities reducer is an instance of an immutable data structure EntityStore. You will want to create the initial state by running EntityStore.create with all of the schemas you defined in the above file.

You will want to then create a reducer function and return a new EntityStore instance by using any number of the fluent prototype methods that are provided.

// reducers/entities.js

import { EntityStore } from 'redux-entity';
import { User, Listing, Reservation, SearchResults } from '../appSchema';

const initialState = EntityStore.create({
  entities: [
    Listing,
    User,
    Reservation,
  ],
  resources: [
    SearchResults,
  ],
});

export default function reducer(state = initialState, action) {
  const { type, payload } = action;
  switch (type) {
    case USER_FETCH:
      return state.setIsLoading(User, payload.id, true);

    case USER_FETCH_SUCCESS:
      return state
        .update(User, payload)
        .setIsLoading(User, payload.id, false);
    
    case LISTING_FETCH:
      return state.setIsLoading(Listing, payload.id, true);

    case LISTING_FETCH_SUCCESS:
      return state
        .update(Listing, payload)
        .setIsLoading(Listing, payload.id, false);
    
    case UPDATE_LISTING_TITLE:
      return state
        .addOptimisticUpdate(Listing, payload.id, action.requestId, {
          title: payload.title,
        });

    case UPDATE_LISTING_TITLE_SUCCESS:
      return state
        .removeOptimisticUpdate(action.requestId)
        .update(Listing, payload);
        
    case SEARCH_FETCH:
      return state.for(SearchResults, sr => sr
        .setIsPageLoading(payload.filter, payload.page, true));

    case SEARCH_FETCH_SUCCESS:
      return state.for(SearchResults, sr => sr
        .setIsPageLoading(payload.filter, payload.page, true)
        .setPage(payload.filter, payload.page, payload.results));

    default: 
      return state;
  }
}

Finally, when connecting with your React components, you will want to use one of the many getter prototype methods from your EntityStore state atom.

// components/ListingContainer.js

import { connect } from 'react-redux';
import { Listing } from '../appSchema';
import ListingComponent from './ListingComponent';

function mapStateToProps({ entities }, props) {
  return {
    listing: entities.get(Listing, props.id),
    isLoading: entities.getIsLoading(Listing, props.id),
  };
}

export default connect(mapStateToProps)(ListingComponent);
// components/Search.js

import { connect } from 'react-redux';
import { SearchResults } from '../appSchema';
import SearchComponent from './SearchComponent';

function mapStateToProps({ entities }, props) {
  const searchResults = entities.get(SearchResults);
  return {
    loading: searchResults.getIsLoading(props.filter, props.page),
    results: searchResults.getPage(props.filter, props.page),
  };
}

export default connect(mapStateToProps)(SearchComponent);

Alternatively, redux-entity exposes a connect HOC similar to react-redux that works directly with GraphQL queries (as opposed to mapStateToProps functions) to the global state atom. This allows for you to grab data for a component that traverses that state graph (which has been normalized by redux-entity) without worrying at all about things such as the optimistic updates, or referential consistency.

// components/Search.js

import { ql, connect } from 'redux-entity';
import SearchComponent from './SearchComponent';

export default connect(ql`{
  searchResults(filter: $filter) {
    totalCount
    isLoading
    listings {
      id
      title
    }
  }
}`)(SearchComponent);
// components/Listing.js

import { ql, connect } from 'redux-entity';
import ListingComponent from './ListingComponent';

export default connect(ql`{
  listing(id: $id) {
    id
    title
    host {
      id
      name
    }
  }
}`)(ListingComponent);

A note on "Resources"

Storing entites in a normalized fashion is only half of the battle. And, quite frankly, it's the simpler half. Most of a real app is a set of "queries" or "resources" that are slices of the global state atom. You usually want to look at state through some sort of lens that makes it useful to the user. You should view Resource classes as the way to do that.

Since they are so important, redux entity provides a way for you to provide your own resource definitions for when the built in ones don't suit your needs...

Below is how you would create a new resource class:

// extensions/MyCustomResource.js

import { 
  Resource, 
  ResourceStore, 
  EntityStore, 
  normalize,
  Schema,
} from 'redux-entity';

class MyCustomResourceStore<T> extends ResourceStore<T> {
  /*
    Here is where you would want to put custom prototype methods that
    return useful information about your resource from the global state
    atom (accessible from `this.state`, or as "fluent" prototype methods
    that return a new global state atom through the usage of the
    `this.fluent(...)` prototype method defined in the `ResourceStore`
    base class.
  */
}

class MyCustomResource<T> extends Resource<T, MyCustomResourceStore<T>> {
  constructor(key: string, itemSchema: Schema<T>) {
    super(key, itemSchema, MyCustomResourceStore);
  }
}

module.exports = MyCustomResource;

Inspiration and Sources

This project (still a work in progress) essentially started as a fork of normalizr. The project was forked because data normalization is slowly going to become only a small feature of this library, and the internals of the code has already changed a fair amount. The ideas and concepts behind this code are highly inspired from the work on normalizr, and how to combine it with redux and immutable in an intelligent way that can reduce the complexity of large modern code bases.

More Repositories

1

react-primitives

Primitive React Interfaces Across Targets
JavaScript
3,085
star
2

redux-pack

Sensible promise handling and middleware for redux
JavaScript
1,333
star
3

react-native-parallax-view

Parallax view for vertical scrollview/listviews with a header image and header content
JavaScript
1,284
star
4

redux-injectable-store

Redux store with injectable reducers for use with bundle splitting, large apps, and SPAs.
JavaScript
230
star
5

react-native-segmented-view

Segmented View for React Native (with animation)
JavaScript
187
star
6

compose-dogfooding

Code written during the Compose Dogfooding streams
Kotlin
183
star
7

react-native-pan-controller

A react native component to help with common use cases for scrolling/panning/etc
JavaScript
181
star
8

recoil

Swift and Kotlin ports of React (Prototype)
Swift
180
star
9

dolla

A light-weight jQuery clone just cuz
JavaScript
139
star
10

enzyme-example-mocha

Example project with React + Enzyme + Mocha
JavaScript
132
star
11

react-native-safe-module

A safe way to consume React Native NativeModules
JavaScript
118
star
12

enzyme-example-react-native

Example project with React Native + Enzyme
Objective-C
87
star
13

native-navigation-boilerplate

Objective-C
82
star
14

enzyme-example-karma-webpack

Example project with React + Enzyme + Karma + Webpack
JavaScript
82
star
15

react_native_animation_examples

JavaScript
80
star
16

react-native-in-depth

Courseware and material for a class on React Native
Objective-C
74
star
17

react-native-future

Some thoughts on future directions and APIs for React Native
67
star
18

enzyme-example-jest

Example project with React + Enzyme + Jest
JavaScript
45
star
19

BQL

BQL: A Better Query Language, SQL Superset
C#
43
star
20

react-image-magnifier

A react component that accepts a high-res source image and produces a magnifier window on mouse hover over the part of the image the cursor is over
JavaScript
39
star
21

LukeMapper

Lucene.Net Document to Object ORM Mapper inspired by Dapper
C#
36
star
22

react-validators

Enhanced React Shape PropType Validators
JavaScript
36
star
23

knockout-react

A wrapper / bridge for using React.js with Knockout and Knockout with React.js
JavaScript
25
star
24

gestio

Declarative DOM-Based Gesture Responder System
JavaScript
21
star
25

style-equal

An efficient equality algorithm for React Native inline styles
JavaScript
20
star
26

astrobin-compose

Kotlin
20
star
27

knockout-components

A components library for Knockout.js (3.0 and above)
JavaScript
18
star
28

knockout-paged

Knockout Extension/Plugin for common paged-data scenarios
CSS
16
star
29

knuckles.js

A web application framework built on top of Knockout.js
JavaScript
13
star
30

react-native-animated-navigator

React Native's Navigator implemented with the Animated API
JavaScript
9
star
31

thinking-in-react

Courseware and material for a class on React
JavaScript
9
star
32

understanding-javascript

Courseware and material for a class on JavaScript
JavaScript
9
star
33

knockout-oftype

Some Extensions and Helpers for handling the Constructor pattern in Knockout
JavaScript
8
star
34

react-primitives-art

Cross-platform interface for react ART library
JavaScript
8
star
35

lucene-fluent-query-builder

A convenient Fluent-API around building Lucene.Net queries
C#
7
star
36

shallow-element-equals

Efficient shallow equality algorithm that also allows checks for react element equality of children props
JavaScript
7
star
37

babel-preset-react-native

Babel preset for react native... with a few tweaks
JavaScript
6
star
38

enzyme-example-karma

Example project with React + Enzyme + Karma
JavaScript
5
star
39

react-blogger

minimal isomorphic blog engine built on react
JavaScript
3
star
40

mixinjs

A small library to create factories with useful mixins-based inheritence. Inspired by React's createClass method.
JavaScript
3
star
41

murmur2js

An optimized JavaScript implementation of the MurmurHash algorithm.
JavaScript
3
star
42

MiniBlog

Low-Profile blogging framework built in ASP.Net WebPages Framework
JavaScript
3
star
43

keynote-highlight

Tiny web app to help with syntax highlighting code in keynote
HTML
2
star
44

sivi

Automation Software for Siempre Viva Remote Observatory
C++
2
star
45

lelandrichardson.github.io

Source code for intelligiblebabble.com
JavaScript
2
star
46

viki

Viki - The Visual Wiki Platform
JavaScript
2
star
47

funky.js

A functional programming library for JavaScript
JavaScript
1
star
48

gift-wrap

An asset bundler and minifier (CSS, JS, LESS) for ASP.Net MVC projects
C#
1
star
49

native-navigator

A fully "native" and cross platform Navigator for React Native
Objective-C
1
star
50

leap-gesto

JavaScript Gesture API for Leap Motion
JavaScript
1
star
51

Talks

JavaScript
1
star
52

react-native-windowed-listview

Windowed ListView for React Native (experimental)
1
star
53

module-dep-graph

Automatically track a dependency graph of all commonjs modules in an application
JavaScript
1
star
54

relay-global-immutable-store

playing around with a concept of a global store, similar to Relay, with some offline & eventually-consistent features
JavaScript
1
star