Security News
ESLint is Now Language-Agnostic: Linting JSON, Markdown, and Beyond
ESLint has added JSON and Markdown linting support with new officially-supported plugins, expanding its versatility beyond JavaScript.
THIS IS A WORK IN PROGRESS!
A small, simple and immutable ORM to manage the entities in your Redux store. redux-orm
doesn't mutate the data, it only returns a new database state.
I got tired of the boilerplate I was writing for reducers. I wrote long reducers that do pretty much the same thing with very small variations. Immutability helpers make the task easier, but the code is not as expressive since it doesn't implement an abstraction of entities and their relations (state.updateIn([id, 'locations'], [0, 2])
vs Person.objects.get({id}).locations.add(0, 2)
). Hence redux-orm
was born.
import {Schema, fk, many} from 'redux-orm';
const schema = new Schema();
// Define your entities.
schema.define('Person', {
// You only need to define related fields.
location: fk('Location')
friends: many('this'),
}, (state, action, Person, orm) => {
// Define the reducer for this entity.
switch (action.type) {
case ADD_PERSON:
Person.create(action.payload);
break;
case EDIT_PERSON:
// The data isn't mutated, you're still working with the same data
// that you started with.
Person.withId(action.payload.id).update(action.payload);
break;
case DELETE_PERSON:
Person.withId(action.payload.id).delete();
break;
case ADD_FRIENDS:
Person.withId(action.payload.id).friends.add(action.payload.friendIds);
break;
case DELETE_FRIENDS:
Person.withId(action.payload.id).friends.remove(action.payload.friendIds);
break;
default:
return state;
}
// A new database state is returned based on the recorded mutations.
return Person.getNextState();
});
schema.define('Location');
function rootReducer(state, action) {
entities: schema.reducer(),
// All your other reducers go here.
}
// Use the ORM in your Components too.
class App extends React.Component {
render() {
const {entities} = this.props;
const {Person, Location} = schema.from(entities);
const people = Person.filter(person => person.age > 18).toPlain();
const childrenElements = people.map(person => {
return <li>{person.toString()}</li>
});
return <ul>{childrenElements}</ul>;
}
}
The API is heavily inspired by the Django ORM. All edits (delete, create, update, set, orderBy) are deferred to when you call getNextState
on a Model and don't mutate data.
npm install --save redux-orm
Import.
import {Schema, fk, many, oneToOne, Model} from 'redux-orm';
Declare your schema.
const schema = new Schema();
const Person = schema.define('Person', {
friends: many('this'),
location: fk('Location'),
// You may also define your reducer later (i.e. in another file)
// with schema.setReducer('Person', reducerFunc);
}, (state, action, Person, orm) => {
// ... Do your thing here
// At the end, just call `getNextState`.
return Person.getNextState();
});
// You may also specify instance methods if you declare your model with an ES6 class.
class Person extends Model {
static meta() {
return {
name: 'Person',
};
}
static get fields() {
return {
friends: many('this'),
location: fk('Location'),
};
}
static reducer(state, action, Person, session) {
switch (action.type) {
case CREATE_PERSON:
Person.create(action.payload);
break;
case UPDATE_PERSON:
Person.withId(action.payload.id).update(action.payload);
break;
case ADD_LOCATION:
Person.withId(action.payload.id).locations.add(action.payload.locationId);
case REMOVE_LOCATION:
Person.withId(action.payload.id).locations.remove(action.payload.locationId);
case DELETE_PERSON:
Person.withId(action.payload.id).delete();
break;
default:
return state;
}
// You could also save the next state in a variable and do
// any manual manipulation of that state.
return Person.getNextState();
}
toString() {
return this.getFullName();
}
getFullName() {
return `${this.first_name} ${this.last_name}`;
},
}
// You may also declare the related fields here.
Person.fields = {
friends: many('this'),
location: fk('Location'),
};
class Location extends Model {
toString() {
return `${this.city}, ${this.country}`;
}
}
// To make sure your code works mangled and minimized,
// You need to specify the model name with a string.
Location.meta = {name: 'Location'};
Plug the reducer anywhere you like. entities
in the root reducer is a good bet.
function rootReducer(state, action) {
entities: schema.reducer(),
}
Now every time you dispatch an action, the Person reducer you defined will be called. Any changes you make to many-to-many relations in the reducer will also be applied. However, you can't (and shouldn't) record mutations for any neighboring Models that are not M2M. They will not be applied. You should do that in the Model's own reducer.
After an action dispatch triggers a store change in redux
, you can instantiate a session in a React component to query the Model instances and use any of the instance methods you declared.
If you're storing items in your redux
state tree this way:
const tree = {
people: [0, 1, 2],
peopleById: {
0: {
name: 'Tommi',
age: 25
},
1: {
name: 'John',
age: 35
},
2: {
name: 'Mary',
age: 30
}
}
};
You'll end up writing quite a bit of boilerplate to handle create, update and delete operations, especially if you're doing it in pure JS.
function peopleReducer(state, action) {
switch (action.type) {
case CREATE_PERSON:
return [...state, action.payload.id];
case DELETE_PERSON:
const personIdx = state.indexOf(action.payload);
return [...state.slice(0, personIdx), ...state.slice(personIdx + 1)];
default:
return state;
}
}
function peopleByIdReducer(state, action) {
switch (action.type) {
case CREATE_PERSON:
return {
...state,
[action.payload.id]: omit(action.payload, 'id'),
};
case DELETE_PERSON:
return omit(state, action.payload);
case UPDATE_PERSON:
const prevPerson = state[action.payload.id];
return {
...state,
[action.payload.id]: Object.assign({}, prevPerson, omit(action.payload, 'id')),
};
default:
return state;
}
}
If you have different entity types, you'll be writing a lot of boilerplate. If you have relations that you need to tip-toe with in the reducers, you'll have to write variations of that boilerplate. The bugs are bound to creep in at some point.
Here's the same logic with redux-orm
:
function peopleReducer(state, action, Person) {
switch (action.type) {
case CREATE_PERSON:
Person.create(action.payload);
break;
case UPDATE_PERSON:
Person.withId(action.payload.id).update(action.payload);
break;
case DELETE_PERSON:
Person.withId(action.payload.id).delete();
break;
default:
return state;
}
return Person.getNextState();
}
Now that's terse.
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. If you need better performance, you can subclass Meta
which defines the data structure and it's access and update functions. It's not that hard, because the database is just a JavaScript object store.
See the full documentation for Schema here
See the full documentation for Model
here.
import {Model, Schema} from 'redux-orm';
class Person extends Model {
static meta() {
return {
name: 'Person';
}
}
toString() {
return `${this.name}, age ${this.age}`;
}
}
const schema = new Schema();
schema.register(Person);
// Assume this is our starting state:
// const startingState = {
// Person: {
// items: [0],
// itemsById: {
// 0: {
// id: 0,
// name: 'Tommi',
// age: 25,
// },
// },
// },
// }
schema.from(startingState);
// person is an instance of `Model`.
const person = schema.Person.get({name: 'Tommi'});
// Access fields like you would in a normal object.
person.age;
// 25
// To get the JavaScript object presentation, use `toPlain`:
person.toPlain()
// returns {
// id: 0,
// name: 'Tommi',
// age: 25
// }
// You can use the instance methods you declared in the class.
person.toString();
// returns "Tommi, age 25"
// You can only record a mutation.
// It won't be applied yet.
person.name = 'Matt';
// undefined
// You can see that nothing has changed in the Model instance.
person.name
// 'Tommi'
// Likewise deleting doesn't mutate state.
person.delete();
person.name
// 'Tommi'
// The changes will be apparent in the new tree returned
// by `schema.getNextState()`
schema.getNextState();
// {
// Person: {
// items: [],
// itemsById: {},
// },
// }
Class Methods:
get
to get a Model instance based on matching properties,create
to create a new Model instance. The new id
will be Math.max(...allOtherIds) + 1
unless you set it explicitly or override the manager's nextId
method.You will also have access to all QuerySet instance methods from the class object for convenience.
Instance methods:
toPlain
: returns a plain JavaScript object presentation of the Model.set
: marks a supplied propertyName
to be updated to value
at Model.getNextState
. Returns undefined
update
: marks a supplied object of property names and values to be merged with the Model instance at Model.getNextState
. Returns undefined
.delete
: marks the Model instance to be deleted at Model.getNextState
. Returns undefined
.See the full documentation for Meta here
See the full documentation for Session here
See the full documentation for QuerySet
here.
You can access all of these methods straight from a Model
class.
Methods:
toPlain
: returns the QuerySet
entities as an array of objects.count
: returns the number of entities in the QuerySet
.exists
: return true
if number of entities is more than 0, else false
.filter
: returns a new QuerySet
with the entities that pass the filter. You can either pass an object that redux-orm
tries to match to the entities, or a function that returns true
if you want to have it in the new QuerySet
, false
if not.exclude
returns a new QuerySet
with the entities that do not pass the filter. Similarly to filter
, you may pass an object for matching (all entities that match will not be in the new QuerySet
) or a function.map
maps through all Model instances.all
returns a new QuerySet
with the same entities.at
returns an Model
instance at the supplied index in the QuerySet
.first
returns an Model
instance at the 0
index.last
returns an Model
instance at the querySet.count() - 1
index.delete
marks all the QuerySet
entities for deletion on Model.getNextState
.update
marks all the QuerySet
entities for an update based on the supplied argument. The argument can either be an object that will be merged with the entity, or a mapping function that takes the entity as an argument and returns a new, updated entity. Do not mutate the entity if you pass a function to update
.MIT. See LICENSE
FAQs
Simple ORM to manage and query your state trees
The npm package redux-orm receives a total of 2,652 weekly downloads. As such, redux-orm popularity was classified as popular.
We found that redux-orm demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 4 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
ESLint has added JSON and Markdown linting support with new officially-supported plugins, expanding its versatility beyond JavaScript.
Security News
Members Hub is conducting large-scale campaigns to artificially boost Discord server metrics, undermining community trust and platform integrity.
Security News
NIST has failed to meet its self-imposed deadline of clearing the NVD's backlog by the end of the fiscal year. Meanwhile, CVE's awaiting analysis have increased by 33% since June.