- Notifications
You must be signed in to change notification settings - Fork114
NOT MAINTAINED – A small, simple and immutable ORM to manage relational data in your Redux store.
License
redux-orm/redux-orm
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
npm install redux-orm --save
Or with a script tag exposing a global calledReduxOrm:
<scriptsrc="https://unpkg.com/redux-orm/dist/redux-orm.min.js"></script>
Latest browser build (only use if size does not matter)
Redux-ORM uses some ES2015+ features, such asSet. If you are using Redux-ORM in a pre-ES2015+ environment, you should load a polyfill likebabel-polyfill before using Redux-ORM.
redux-orm-proptypes: React PropTypes validation and defaultProps mixin for Redux-ORM Models
For a detailed walkthrough seea guide to creating a simple app with Redux-ORM. Its not up-to-date yet but thecode has a branch for version 0.9. The Redux docs have ashort section on Redux-ORM as well.
You can declare your models with the ES6 class syntax, extending fromModel. You need to declare all your non-relational fields on the Model, and declaring all data fields is recommended as the library doesn't have to redefine getters and setters when instantiating Models. Redux-ORM supports one-to-one and many-to-many relations in addition to foreign keys (oneToOne,many andfk imports respectively). Non-related properties can be accessed like in normal JavaScript objects.
// models.jsimport{Model,fk,many,attr}from'redux-orm';classBookextendsModel{toString(){return`Book:${this.name}`;}// Declare any static or instance methods you need.}Book.modelName='Book';// Declare your related fields.Book.fields={id:attr(),// non-relational field for any value; optional but highly recommendedname:attr(),// foreign key fieldpublisherId:fk({to:'Publisher',as:'publisher',relatedName:'books',}),authors:many('Author','books'),};exportdefaultBook;
Defining fields on a Model specifies the table structure in the database for that Model. In order to generate a description of the whole database's structure, we need a central place to register all Models we want to use.
An instance of the ORM class registers Models and handles generating a full schema from all the models and passing that information to the database. Often you'll want to have a file where you can import a single ORM instance across the app, like this:
// orm.jsimport{ORM}from'redux-orm';import{Book,Author,Publisher}from'./models';constorm=newORM({stateSelector:state=>state.orm,});orm.register(Book,Author,Publisher);exportdefaultorm;
You could also defineand register the models to an ORM instance in the same file, and export them all.
Now that we've registered Models, we can generate an empty database state. Currently that's a plain, nested JavaScript object that is structured similarly to relational databases.
// index.jsimportormfrom'./orm';constemptyDBState=orm.getEmptyState();
When we have a database state, we can start an ORM session on that to apply updates. The ORM instance provides asession method that accepts a database state as it's sole argument, and returns a Session instance.
constsession=orm.session(emptyDBState);
Session-specific classes of registered Models are available as properties of the session object.
constBook=session.Book;
Models provide an interface to query and update the database state.
Book.withId(1).update({name:'Clean Code'});Book.all().filter(book=>book.name==='Clean Code').delete();Book.idExists(1)// false
The initial database state is not mutated. A new database state with the updates applied can be found on thestate property of the Session instance.
constupdatedDBState=session.state;
To integrate Redux-ORM with Redux at the most basic level, you can define a reducer that instantiates a session from the database state held in the Redux state slice, then when you've applied all of your updates, you can return the next state from the session.
importormfrom'./orm';functionormReducer(dbState,action){constsess=orm.session(dbState);// Session-specific Models are available// as properties on the Session instance.const{ Book}=sess;switch(action.type){case'CREATE_BOOK':Book.create(action.payload);break;case'UPDATE_BOOK':Book.withId(action.payload.id).update(action.payload);break;case'REMOVE_BOOK':Book.withId(action.payload.id).delete();break;case'ADD_AUTHOR_TO_BOOK':Book.withId(action.payload.bookId).authors.add(action.payload.author);break;case'REMOVE_AUTHOR_FROM_BOOK':Book.withId(action.payload.bookId).authors.remove(action.payload.authorId);break;case'ASSIGN_PUBLISHER':Book.withId(action.payload.bookId).publisherId=action.payload.publisherId;break;}// the state property of Session always points to the current database.// Updates don't mutate the original state, so this reference is not// equal to `dbState` that was an argument to this reducer.returnsess.state;}
Previously we advocated for reducers specific to Models by attaching a staticreducer function on the Model class. If you want to define your update logic on the Model classes, you can specify areducer static method on your model which accepts the action as the first argument, the session-specific Model as the second, and the whole session as the third.
classBookextendsModel{staticreducer(action,Book,session){switch(action.type){case'CREATE_BOOK':Book.create(action.payload);break;case'UPDATE_BOOK':Book.withId(action.payload.id).update(action.payload);break;case'REMOVE_BOOK':constbook=Book.withId(action.payload);book.delete();break;case'ADD_AUTHOR_TO_BOOK':Book.withId(action.payload.bookId).authors.add(action.payload.author);break;case'REMOVE_AUTHOR_FROM_BOOK':Book.withId(action.payload.bookId).authors.remove(action.payload.authorId);break;case'ASSIGN_PUBLISHER':Book.withId(action.payload.bookId).publisherId=action.payload.publisherId;break;}// Return value is ignored.returnundefined;}toString(){return`Book:${this.name}`;}}
To get a reducer for Redux that calls thesereducer methods:
import{createReducer}from'redux-orm';importormfrom'./orm';constreducer=createReducer(orm);
This reducer needs to be hooked into your Redux store. Make sure that the key under which you store it is also the key that you use to retrieve the ORM's state in itsstateSelector. Otherwise selectors won't work properly.
createReducer is really simple, so we'll just paste the source here.
functioncreateReducer(orm,updater=defaultUpdater){return(state,action)=>{constsession=orm.session(state||orm.getEmptyState());updater(session,action);returnsession.state;};}functiondefaultUpdater(session,action){session.sessionBoundModels.forEach(modelClass=>{if(typeofmodelClass.reducer==='function'){modelClass.reducer(action,modelClass,session);}});}
As you can see, it just instantiates a new Session, loops through all the Models in the session, and calls thereducer method if it exists. Then it returns the new database state that has all the updates applied.
Use memoized selectors to make queries into the state. Redux-ORM uses smart memoization: the below selector accessesAuthor andAuthorBooks branches (AuthorBooks is a many-to-many branch generated from the model field declarations), and the selector will be recomputed only if those branches change. The accessed branches are resolved on the first run.
// selectors.jsimport{createSelector}from'redux-orm';importormfrom'./orm';constauthorSelector=createSelector(orm,session=>{returnsession.Author.all().toModelArray().map(author=>{/** * author is a model instance and exposes relationship accessors * such as author.books … * * This gets a reference to the model's underlying object * which has no such accessors, containing only raw attributes. */const{ ref}=author;// Object.keys(ref) === ['id', 'name']return{ ...ref,books:author.books.toRefArray().map(book=>book.name),};});});// Will result in something like this when run:// [// {// id: 0,// name: 'Tommi Kaikkonen',// books: ['Introduction to Redux-ORM', 'Developing Redux applications'],// },// {// id: 1,// name: 'John Doe',// books: ['John Doe: an Autobiography']// }// ]
Selectors created withcreateSelector can be used as input to any additionalreselect selectors you want to use. They are also great to use withredux-thunk: get the whole state withgetState(), pass the ORM branch to the selector, and get your results. A good use case is serializing data to a custom format for a 3rd party API call.
Because selectors are memoized, you can use pure rendering in React for performance gains.
// components.jsimportReactfrom'react';import{authorSelector}from'./selectors';import{connect}from'react-redux';functionAuthorList({ authors}){constitems=authors.map(author=>(<likey={author.id}>{author.name} has written{author.books.join(', ')}</li>));return(<ul>{items}</ul>);}functionmapStateToProps(state){return{authors:authorSelector(state),};}exportdefaultconnect(mapStateToProps)(AuthorList);
Well, yeah. Redux-ORM deals with related data, structured similar to a relational database. The database in this case is a simple JavaScript object database.
For simple apps, writing reducers by hand is alright, but when the number of object types you have increases and you need to maintain relations between them, things get hairy. ImmutableJS goes a long way to reduce complexity in your reducers, but Redux-ORM is specialized for relational data.
Say we start a session from an initial database state situated in the Redux atom, update the name of a certain book.
First, a new session:
import{orm}from'./models';constdbState=store.getState().orm;// getState() returns the Redux stateconstsess=orm.session(dbState);
The session maintains a reference to a database state. We haven'tupdated the database state, therefore it is still equal to the originalstate.
sess.state===dbState// true
Let's apply an update.
constbook=sess.Book.withId(1)book.name// 'Refactoring'book.name='Clean Code'book.name// 'Clean Code'sess.state===dbState// false
The update was applied, and because the session does not mutate the original state, it created a new one and swappedsess.state to point to the new one.
Let's update the database state again through the ORM.
// Save this reference so we can compare.constupdatedState=sess.state;book.name='Patterns of Enterprise Application Architecture';sess.state===updatedState// true. If possible, future updates are applied with mutations. If you want// to avoid making mutations to a session state, take the session state// and start a new session with that state.
If possible, future updates are applied with mutations. In this case, the database was already mutated, so the pointer doesn't need to change. If you want to avoid making mutations to a session state, take the session state and start a new session with that state.
Just like you can extendModel, you can do the same forQuerySet (customize methods on Model instance collections). You can also specify the whole database implementation yourself (documentation pending).
The ORM abstraction will never be as performant compared to writing reducers by hand, and adds to the build size of your project. If you have very simple data without relations, Redux-ORM may be overkill. The development convenience benefit is considerable though.
See the full documentation for ORMhere
constorm=newORM({stateSelector:state=>state.orm,// wherever the reducer is put during createStore});
register(...models: Array<Model>): registers Model classes to theORMinstance.session(state: any): begins a newSessionwithstate.
createReducer(orm: ORM): returns a reducer function that can be plugged into Redux. The reducer will return the next state of the database given the provided action. You need to register your models before calling this.createSelector(orm: ORM, [...inputSelectors], selectorFunc): returns a memoized selector function forselectorFunc.selectorFuncreceivessessionas the first argument, followed by any inputs frominputSelectors. Note that the first inputSelector must return the db-state to create a session from. Read the full documentation for details.
See the full documentation forModelhere.
Instantiation: Don't instantiate directly; use the class methodscreate andupsert as documented below.
Class Methods:
withId(id): gets the Model instance with idid.idExists(id): returns a boolean indicating if an entity with ididexists in the state.exists(matchObj): returns a boolean indicating if an entity whose properties matchmatchObjexists in the state.get(matchObj): gets a Model instance based on matching properties inmatchObj(if you are sure there is only one matching instance).create(props): creates a new Model instance withprops. If you don't supply an id, the newidwill beMath.max(...allOtherIds) + 1.upsert(props): either creates a new Model instance withpropsor, in case an instance with the same id already exists, updates that one - in other words it'screate or update behaviour.
You will also have access to almost allQuerySet instance methods from the class object for convenience, includingwhere and the like.
ref: returns a direct reference to the plain JavaScript object representing the Model instance in the store.
equals(otherModel): returns a boolean indicating equality withotherModel. Equality is determined by shallow comparison of both model's attributes.set(propertyName, value): updatespropertyNametovalue. Returnsundefined. Is equivalent to normal assignment.update(mergeObj): mergesmergeObjwith the Model instance properties. Returnsundefined.delete(): deletes the record for this Model instance in the database. Returnsundefined.
Use the ES6 syntax to subclass fromModel. Any instance methods you declare will be available on Model instances. Any static methods you declare will be available on the Model class in Sessions.
For the related fields declarations, either set thefields property on the class or declare a static getter that returns the field declarations like this:
classBookextendsModel{staticgetfields(){return{id:attr(),name:attr(),author:fk('Author'),};}}// alternative:Book.fields={id:attr(),name:attr(),author:fk('Author'),}
All the fieldsfk,oneToOne andmany accept a single argument, the related model name. The fields will be available as properties on eachModel instance. You can set related fields with the id value of the related instance, or the related instance itself.
Forfk, you can access the reverse relation throughauthor.bookSet, where the related name is${modelName}Set. Same goes formany. ForoneToOne, the reverse relation can be accessed by just the model name the field was declared on:author.book.
Formany field declarations, accessing the field on a Model instance will return aQuerySet with two additional methods:add andremove. They take 1 or more arguments, where the arguments are either Model instances or their id's. Calling these methods records updates that will be reflected in the next state.
Relations support more configuration options like accessors, related names, etc. by passing an object:
classBookextendsModel{staticgetfields(){return{id:attr(),name:attr(),authorId:fk({to:'Author',as:'author',relatedName:'writtenBooks'}),reviewerIds:many({to:'Author',as:'reviewers',relatedName:'reviewedBooks'})};}}
Seefk,oneToOne, andmany in the documentation for more information.
When declaring model classes, always remember to set themodelName property. It needs to be set explicitly, because running your code through a mangler would otherwise break functionality. ThemodelName will be used to resolve all related fields.
classBookextendsModel{staticgetmodelName(){return'Book';}}// alternative:Book.modelName='Book';
If you need to specify options to the Redux-ORM database, you can declare a staticoptions property on the Model class with an object key.
// These are the default values.Book.options={idAttribute:'id',mapName:'itemsById',arrName:'items',};
See the full documentation forQuerySethere.
You can access all of these methods straight from aModel class, as if they were class methods onModel. In this case the functions will operate on a QuerySet that includes all the Model instances.
toRefArray(): returns the objects represented by theQuerySetas an array of plain JavaScript objects. The objects are direct references to the store.toModelArray(): returns the objects represented by theQuerySetas an array ofModelinstances objects.count(): returns the number ofModelinstances in theQuerySet.exists(): returntrueif number of entities is more than 0, elsefalse.filter(filterArg): returns a newQuerySetrepresenting the records from the parent QuerySet that pass the filter. ForfilterArg, you can either pass an object that Redux-ORM tries to match to the entities, or a function that returnstrueif you want to have it in the newQuerySet,falseif not. The function receives a model instance as its sole argument.excludereturns a newQuerySetrepreseting entities in the parent QuerySet that do not pass the filter. Similarly tofilter, you may pass an object for matching (all entities that match will not be in the newQuerySet) or a function. The function receives a model instance as its sole argument.all()returns a newQuerySetwith the same entities.at(index)returns anModelinstance at the suppliedindexin theQuerySet.first()returns anModelinstance at the0index.last()returns anModelinstance at thequerySet.count() - 1index.delete()deleted all entities represented by theQuerySet.update(mergeObj)updates all entities represented by theQuerySetbased on the supplied object. The object will be merged with each entity.
See the full documentation for Sessionhere
You don't need to do this yourself. Useorm.session (usually what you want) ororm.mutableSession.
state: the current database state in the session.
Additionally, you can access all the registered Models in the schema for querying and updates as properties of this instance. For example, given a schema withBook andAuthor models,
constsession=orm.session(state);session.Book// Model class: Booksession.Author// Model class: Authorsession.Book.create({id:5,name:'Refactoring',release_year:1999});
API is still unstable. Minor changes before v1.0.0 can and will include breaking changes, adhering to Semantic Versioning.
SeeCHANGELOG.md.
The 0.9.x versions brought big breaking changes to the API. Please look at themigration guide if you're migrating from earlier versions.
Looking for the 0.8 docs? Read theold README.md in the repo. For the API reference, clone the repo,npm install,make build and open upindex.html in your browser. Sorry for the inconvenience.
MIT. SeeLICENSE.
About
NOT MAINTAINED – A small, simple and immutable ORM to manage relational data in your Redux store.
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.