realm / realm-js

Realm is a mobile database: an alternative to SQLite & key-value stores
https://realm.io
Apache License 2.0
5.62k stars 558 forks source link

Add a React/Redux example #141

Open alazier opened 8 years ago

jwhitley commented 8 years ago

+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:

  1. The entire app state is represented in a single state tree.
  2. The state tree is mutated by a (possibly hierarchical) pure reducer function of form
    (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?

appden commented 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.

robwalkerco commented 8 years ago

+1

jwhitley commented 8 years ago

@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.

alazier commented 8 years ago

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.

fungilation commented 8 years ago

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.

jwhitley commented 8 years ago

@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-run render() by calling forceUpdate(). [...] Normally you should try to avoid all uses of forceUpdate() and only read from this.props and this.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:

  1. Actions in Redux are data rather than functions or methods. This opens up opportunities for logging, middleware, history manipulation, and more.
  2. Following on no. 1, employing redux-saga means the workflows that respond to your actions are also (mostly) data. A saga generator function can be thought of as a function that returns a description of what to do when an action is received. redux-saga is then an engine that reads these descriptions and executes your workflow. That's what makes sagas highly testable. I also love that sagas collect logic that is often scattered around in traditional controllers, delegate methods, etc.. Saga unit tests would often end up being integration tests under other frameworks to cover the same functionality
  3. Even without going down the redux-saga route, the action creator pattern keeps a nice separation between side-effects (in the action creator functions) and the actions themselves (pure data structures).
  4. I think we're going to see a lot more sophistication around the power of Redux-style immutable state and first-class actions over time. A lot of interesting apps eventually have to fight with remote synchronization issues. I've been contemplating interesting possibilities around connecting a subset of actions and an Operational Transformation layer, for example.

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.

jwhitley commented 8 years ago

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 };
}
fungilation commented 8 years ago

@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.

MichaelDanielTom commented 8 years ago

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 👍

fungilation commented 8 years ago

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?

MichaelDanielTom commented 8 years ago

@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.

fungilation commented 8 years ago

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

MichaelDanielTom commented 8 years ago

@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!

EngineerDiab commented 7 years ago

How can deep copies be made in realm? I thought only shallow copies were supported to date?

alazier commented 7 years ago

@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.

rturk commented 7 years ago

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.

rturk commented 7 years ago

@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

andrewvmail commented 7 years ago

@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!

g6ling commented 7 years ago

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 });
amanthegreatone commented 7 years ago

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...

  1. 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

  2. fetch the relevant data from realm and get a results object

  3. push that results object into the store with a populateStore action and reducer

  4. use that results object from the store for UI rendering

  5. 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?

lodev09 commented 7 years ago

@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(?)

cwagner22 commented 7 years ago

@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?

lodev09 commented 7 years ago

@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

cwagner22 commented 7 years ago

@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.

lodev09 commented 7 years ago

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 :)

Zhuinden commented 6 years ago

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.

lodev09 commented 6 years ago

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 :)

Zhuinden commented 6 years ago

Ah, that does make sense. :+1:

lodev09 commented 6 years ago

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.

angelstoone commented 6 years ago

What do you think about redux-persist-realm?

fungilation commented 6 years ago

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.

brancooo1 commented 6 years ago

+1

brancooo1 commented 6 years ago

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

fungilation commented 6 years ago

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.

takameyer commented 10 months ago

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!