Open alazier opened 8 years ago
@jwhitley That was a very thorough and accurate overview of the challenges of integrating Realm and Redux. It's actually something we have been actively working towards resolving by allowing for deep snapshots of data inside a Realm. At its core, Realm supports this because of its nature as a write-only database, but there are some challenges we have yet to overcome to properly expose that functionality in a language binding. At the moment, when integrating with Redux, it's probably best to treat Realm purely as a persistence layer, separate from the application state. Once we have support for immutable snapshots, we will create an example app that leverages that functionality to integrate with Redux.
+1
@appden I'm currently implementing the above approach in my app, and have run into an error from Realm: uncaught Error: Value not convertible to a number.
I need a primary key so that I can handle edits of existing objects (i.e. DB updates), via the following usage of create
:
realm.write(() => {
realm.create('Thing', { id: 123, foo: "...", bar: "..." }, true);
})
However, upon the initial creation of an object, I don't have an id
property yet, and realm.create()
emits the Value not convertible to a number
error. This error occurs whether or not I pass true
or false
for the third argument at creation time. Presumably this is because create()
isn't finding a numerical value for id
in the properties of the passed object. If so, the messaging could be vastly improved.
Does Realm require that the app explicitly manage creating unique PKs if they're enabled in a model's schema? If so, that should really be added to the docs. Pushing that logic, traditionally managed by the DB, out onto the app is a non-trivial burden.
Primary key values always need to be provided when calling the create
method. It is currently up to the user to manage primary keys but there are plans to add auto-incrementing primary keys sometime in the future. In the shorter term we may be able to support functions for default values which would allow users to concisely build their own auto-incrementing functionality.
For the time being we will update the documentation with the current limitations.
A corollary, for me just starting with RN. For managing UI states in a brand new RN app, is there benefit to use Redux at all, if/could/should I use Realm entirely for managing UI states? Persistence is what Realm uniquely brings to UI states, so I'm wondering for starting out, should I rely on it for managing UI states entirely or I should still use Redux in conjunction.
Thanks for your thoughts on this.
@fungilation Good question. Here's my take as a Realm user who has been integrating it into a Redux-based app.
tl;dr: I trade off some Realm features in favor of Redux features. Read on for the details.
Realm's current approach is essentially to turn queries into live view objects (Realm.Results
) which drive your UI. The live updates mean that any writes that occur are immediately manifested in existing result sets. However, that side-effect based workflow doesn't provide React with knowledge that anything changed. You can see this in the realm-js example code. Note the call to this.forceUpdate()
in that method. In the React world, calling forceUpdate()
is a code smell:
By default, when your component's state or props change, your component will re-render. However, if these change implicitly (eg: data deep within an object changes without changing the object itself) or if your
render(
) method depends on some other data, you can tell React that it needs to re-runrender()
by callingforceUpdate()
. [...] Normally you should try to avoid all uses offorceUpdate()
and only read fromthis.props
andthis.state
in render().
Realm also has change events, but those are currently a blunt hammer. You register for literally anything that changes, and are given no information other than "something changed". Your only recourse is therefore to re-render the universe.
So on that point, there's some architectural friction between React and Realm right out of the gate. However, realm-js is still quite young, and there are signs that this situation will improve greatly over time.
As for Redux, if you haven't already, definitely give time to Dan Abramov's excellent series of short videos. That introduces not only the basics of Redux, but it helps greatly to understand the motivations behind why it works the way it does. Speaking personally, here are some things I really like about a Redux-based app architecture:
As you can tell, I'm currently a fan of Redux-based apps. Which means that I use Realm just as I described above: like it's a traditional local (or even remote) store. When I query Realm, e.g. realm.objects('Thing')
, I must make a deep copy of the returned results to put in the Redux store. Redux fundamentally assumes an immutable store. If you squint a bit, this is little different than getting some result-set data structure from, e.g. the SQLite library, and mapping that into plain Javascript objects to add to the store.
Following on @alazier's earlier comment, I ended up creating this Sequence object store in my top-level realm schema file. This is a hand-rolled implementation of autoincrementing sequences for realm-js. The Sequence table has keys which are the names of other objects in the schema. The value for each key is the highest used id
primary key for each object type.
import Realm from 'realm'
const SCHEMA_VERSION = 0
class Thing {
}
Thing.schema = {
name: 'Thing',
primaryKey: 'id',
properties: {
id: 'int',
thingProp1: 'string',
thingProp2: 'string',
}
}
class Sequence {
static save(schema, props) {
let saved;
realm.write(() => {
let obj = {...props};
if (obj.id === undefined) {
let seq = realm.objects('Sequence').filtered(`name = "${schema}"`)[0];
if (seq === undefined) {
seq = realm.create('Sequence', { name: schema, value: 0 });
}
obj.id = seq.next();
}
saved = realm.create(schema, obj, true);
})
return {...saved};
}
next() {
this.value = this.value+1;
return this.value;
}
}
Sequence.schema = {
name: 'Sequence',
primaryKey: 'name',
properties: {
name: 'string',
value: 'int',
}
}
const realm = new Realm({
schema: [Thing, Sequence],
schemaVersion: SCHEMA_VERSION
})
export { realm, Sequence };
Then in an action creator (or saga, in my case), saving a Thing might look like this:
function saveThing(athing) {
// athing is just a plain Javascript object with properties to be persisted.
// it may or may not have an 'id' property. If it does, update the existing object
// in the Realm store with the matching id. If not, it's a new object. Assign it a
// unique 'id' and save it.
let saved = Sequence.save('Thing', athing);
return { type: 'SAVE_THING', payload: saved };
}
@jwhitley, thanks for the thorough explanation! Your experience appreciated. I come from Meteor and Realm's reactive data objects is quite familiar and "convenient". Redux's functional and pure approach is harder to wrap my head around but I am already going through Dan Abramov's videos. Looks like using it with React Native is still the way to go, with Realm used to populate initial state, and save to Realm at save points.
Thanks for the Sequence autoincrement class too.
Hey guys, what's your opinion on saving Realm objects into the Redux store vs saving some type of primary key - Realm object type tuple? It seems that in terms of keeping only the bare minimum amount of data in Realm, it would make sense to only save the key, and then get the entire object through a selector. However for actually binding the redux state to the UI, saving the entire object seems a ton easier.
P.S. Thanks for articulating the tradeoffs and options @jwhitley, that was helpful 👍
On a tangent, what about integration with Redux Persist? With its autoRehydrate() and persistStore(), it'd work quite automagically as a drop in for persistence. It currently uses AsyncStorage as storage, is Realm a natural fit here instead?
@fungilation Are you suggesting that all realm integration happens during the persist step, and that UI updates are completely handled by Redux? I feel like simply swapping out AsyncStorage for Realm wouldn't do that much if it's only used as a key-value store.
It does as its drop in for initial integration, while the data can be queried in other ways for new alternate views independent of redux? I haven't thought this out fully so consider this a proposal and discuss on whether such integration is worthwhile to the community. On Sat, Apr 30, 2016 at 11:33 AM Michael Tom notifications@github.com wrote:
@fungilation https://github.com/fungilation Are you suggesting that all realm integration happens during the persist step, and that UI updates are completely handled by Redux? I feel like simply swapping out AsyncStorage for Realm wouldn't do that much if it's only used as a key-value store.
— You are receiving this because you were mentioned. Reply to this email directly or view it on GitHub https://github.com/realm/realm-js/issues/141#issuecomment-215985712
@jwhitley Could you provide an example of how exactly you use Realm as the persistent store, if you're making deep copies into Redux? Are all realm actions defined in sagas, with actions like refreshUser, which makes a network request, saves it to realm, and saves the resulting realm object into the Redux state? How are you handling normalization handled within the redux state?
Sorry for all the questions, but I'm trying to decide whether it would be easier to just use normalizr and Redux and get rid of the Realm layer, or if there will be significant performance benefits in the future from using Realm. Thanks!
How can deep copies be made in realm? I thought only shallow copies were supported to date?
@EngineerDiab at the moment only shallow copies are possible unless you copy data out of the realm. Deep copies is something we are working towards in the longer term though.
Has anyone tested using a component Wrapper
that contains the actual Realm Query and passes down the results to the component that will actually perform the Render
? Provided that the current props vs new props are pure on every event change React should be able to only re-render only what was changed/updated/created.
@jwhitley looks like this PR will address some (if not all) your interesting comments regarding force update
and events subscribers
https://github.com/realm/realm-js/pull/549
@jwhitley I'm also interested in the details on how you use realm in the context of sagas and how you designed the mechanism of synchronization between the redux store and realm. Ie. when the app starts, offline detection etc. Thanks in advance!
In my case, I think realm as API Server. If I send data, I receive the response, and I update my store with the response.
So I made a function called ImmutableRealm
.
// realm/index.js
const realm = new Realm({ schema: [...] });
export const ImmutableRealm = (func, option = {}) => {
const defaultCopy = (item) => JSON.parse(JSON.stringify(item));
const copy = option.copy || defaultCopy; // Use deep copy.
const success = option.success || true;
const fail = option.fail || false;
const defualtErrorHandler = (e) => e;
const errorHandler = option.errorHandler || defualtErrorHandler;
return (props) => new Promise((resolve, reject) => {
try {
const result = func(props, realm) || 'Return is null';
const copiedResult = copy(result);
resolve({ status: success, data: copiedResult });
} catch (e) {
const error = errorHandler(e);
reject({ status: fail, error });
}
});
};
export default realm
and I made Realm's API functions.
export const getFunction = ImmutableRealm(({ id }, realm) => {
let item = realm.object('Name').filtered(`id == ${id}`)
return item;
});
// example Todo
export const getCheckedTodoItem = ImmutableRealm(({ listId }, realm) => {
const list = realm.object('List').filtered(`id == ${listId}`);
const checkedTodos = list.todos.filtered(`check == true`);
return checkedTodos;
});
Use this like API.
If you use redux-saga, you can write like below.
const resRealm = yield call(getCheckedTodoItem, { listId });
trying to wrap my head around this, came across this repo https://github.com/bosung90/RNStorage.
Can someone strip down to fundamentals and explain how the data flows and how data updates are handled (redux store and realm).
Lets say we get data into realm from a server API. What should be sent from realm to the redux store? Should we send Realm Results or strip down the values and populate normal objects (that dont autoupdate like realm result objects) via redux actions? what should the reducers do?
here's what i think and where i'm stuck...
Get the data from API and push to realm ... lets say its an object having a set of properties that can be modified from the UI
fetch the relevant data from realm and get a results object
push that results object into the store with a populateStore action and reducer
use that results object from the store for UI rendering
This is where I'm stuck...in the UI when something happens to update the state, should we just use the results object from the state and update it directly (result.property = new value) and not have any force updates for the UI? or should we do a thunk action that pushes the change to realm and the corresponding reducer does not have to do anything as the changes reflects automatically in the state because of results object and its autoupdate property?
@jwhitley I'm doing exactly what you pointed above before I saw your post. I was googling if I'm doing the "right" thing too with realm + redux and found your post. Definitely ease my doubt with my implementation.
You're right about react-native and redux not knowing that something added/removed to realm's Results
so I'm passing it as a "new" state to the reducer so UI gets updated.
I hope realm
supports redux in the future -- maybe for performance/efficiency purposes(?)
@g6ling With your ImmutableRealm
function all the arrays (realm lists) are converted to object wich is a bit of a problem because I have to manually convert them back to array. It's the same issue if I use another Immutable
library. Any simple way to fix that?
@cwagner22 I don't think you need to convert the realm results at all. I do this on my app i.e. I pass the results/object (from filtered
, etc) directly to the reducer and I can still access properties and methods I defined on the objects. But as mentioned above, the objects are not "live" so updates with realm doesn't reflect without a dispatch
. I guess redux does its thing already in making the objects "immutable" but still they are realm objects :)
EDIT: I may have explained it wrong about the "live" thing... I was referring to the rendering mechanism. If you update an object via realm, it actually gets reflected but you'll have to dispatch
with redux to trigger a render
@lodev09 Thanks. Well it looks like the root of my issue is that realm.objects('Car')
returns an object and not an array. Same for nested lists. So even if I try to save the results directly in redux I have the same issue.
Side question: When you mention
"methods I defined on the objects."
Do you mean you have created some custom methods for your realm objects? I would love to see an example because I didn't find any way to do that.
returns an object and not an array
In the realm world, their "results" acts like an array actually i.e. you can do forEach
, map
, etc. I would suggest you look a their docs for realm-js. They have their own version of a ListView
for optimization (standard listview works as well).
Do you mean you have created some custom methods for your realm objects?
yes you can.. I've been doing this to make it more of a standard object instead of an object for realm. You are defining a class
so you can just put methods in it and you can call them like a normal object.
Here's an example object definition:
// define your objects
class Car {
static schema = {
name: 'Car',
primaryKey: 'id',
properties: {
id: 'int',
model: 'string',
name: 'string'
// ...
}
}
// static methods
static getCar(id) {
// see definition below
return Car.getFromId(id);
}
// a method
changeName(name) {
realm.write(() => {
this.name = name;
});
}
}
class Person {
// same stuff
}
const schemas = [
Car,
Person
];
// create the realm
const realm = new Realm({
schema: schemas
});
// this is kinda hacky
// this will basically inject static methods for common realm methods
schemas.forEach((ObjectType) => {
const schemaName = ObjectType.schema.name;
ObjectType.get = function() {
return realm.objects(schemaName);
}
ObjectType.getFromId = function(id) {
return realm.objectForPrimaryKey(schemaName, id);
}
// your common realm methods here like inserts, updates, etc.
// ...
});
Now you can do this:
const cars = Car.get();
if (cars) {
cars.forEach((car, i) => {
car.changeName('car index ' + i);
});
}
// get a single car
const car = Car.getFromId(123);
car.changeName('car 123');
I haven't tested that code but I hope that works for you :)
Redux demands that there is only 1 store, and that all previous states can be re-built from operation history.
Realm works as a store (change event emission on changes) but it doesn't retain history, so it cannot really be used as a redux store. It's more like a flux-store in that regard.
I don't think Realm and Redux are conceptually compatible, although you can create a uni-directional architecture with it if you truly want: https://academy.realm.io/posts/eric-maxwell-uni-directional-architecture-android-using-realm/ otherwise you have to detach the objects to have a history of previous states.
I'm treating realm as "extra" functionality to my models, mainly for offline support.
Also, I've refactored my code a long time ago as I've found out that it's not performant to directly pass along realm objects through redux. In short, redux should only accept "plain" objects. A helper method for each model would be useful to map "plain" properties.
During actions, IDs are passed along and recreating a realm object from it if needed.
Hope this helps someone :)
Ah, that does make sense. :+1:
In case anyone is wondering how I "map" properties for redux state objects, here is an updated code from above with property mapping that I use currently.
class Car extends Realm.Object {
static schema = {
name: 'Car',
primaryKey: 'id',
properties: {
id: 'int',
model: 'string',
name: 'string'
// ...
}
}
// static methods
static getCar(id) {
// see definition below
return Car.getFromId(id);
}
// a method
changeName(name) {
realm.write(() => {
this.name = name;
});
}
// here is where we call the mapping of "pure" properties for redux
props() {
return Car.mapProps(this);
}
}
class Person extends Realm.Object {
// same stuff
}
const schemas = [
Car,
Person
];
// create the realm
const realm = new Realm({
schema: schemas
});
// this is kinda hacky
// this will basically inject static methods for common realm methods
schemas.forEach((ObjectType) => {
const schemaName = ObjectType.schema.name;
ObjectType.get = function() {
return realm.objects(schemaName);
}
ObjectType.getFromId = function(id) {
return realm.objectForPrimaryKey(schemaName, id);
}
// the static method that can be used for every realm objects
ObjectType.mapProps = function(object, exclude = []) {
let props = {};
const propNames = Object.keys(ObjectType.schema.properties).filter(p => exclude.indexOf(p) < 0);
propNames.forEach((p) => {
if (typeof object[p] !== 'function') {
const propSchema = ObjectType.schema.properties[p];
let type = null;
if (typeof propSchema === 'string') {
type = propSchema;
} else {
type = propSchema.type;
}
switch (type) {
case 'date':
props[p] = object[p] && object[p].getTime();
break;
default:
props[p] = object[p];
break;
}
}
});
return props;
}
// your common realm methods here like inserts, updates, etc.
// ...
});
Reducer Action side:
// actions
dispatch({
type: GET_CAR,
car: car.props()
});
Feel free to modify or comment what you think. 😄
EDIT:
As mentioned from redux docs here, it's best to call the props
method in the action side instead in reducer.
What do you think about redux-persist-realm?
Intriguing! A new project I see. I've been looking for tying redux and realm, using Realm to persist Redux is a natural way to integrate.
+1
After bit of investigating I went with redux-thunk - realm solution. I personally like more the idea.
Here's blog post where I got inspiration: https://medium.com/@manggit/react-native-redux-realm-js-r3-js-a-new-mobile-development-standard-5290ec02a590
I'm now undecided between Firebase (firestore) and realm. I'm already using Firebase without integration with redux. And redux-thunk does look like the best/simplest way to integrate, either one.
Hey all. It's been a while since this issue has been commented on. We have since release @realm/react
which provides contextual hooks to gain access to Realm within React Native. The goal is to be able to use this without the overhead of tying Realm to a state management library. Let us know what you think!
+1 I'm really quite curious as to what the envisioned integration between Realm and Redux would be.
As each of Redux and Realm are presented, they seem to be at odds. On one hand, Redux depends on two things:
(oldState, action) => newState
On the other, Realm seems to be designed around the principle that components access Realm data via persistent results, effectively data views, which are auto-updated. A naive merging of a Redux-based approach and the React example code in this repo, would turn Realm into a second repository of application state which muddies the entire story around Redux and its related tools.
Since application state would no longer be fully represented in Redux, it's unclear how Redux patterns like redux-saga, redux-undo, could work properly anymore. Effectively, reducers can't even touch persistent data state in Realm, since they need to be pure functions.
One could resolve this conflict by treating Realm like any other store, pushed to the effect-ful edge of a React/Redux app, e.g. where an action creator or saga marshals effects, dispatches actions, and otherwise handles workflow in a Redux-friendly manner.
That raises the following question: is there some better, more elegant, integration of Realm into a Redux app?