realm / realm-js

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

Update example apps to show Realm integrated with the latest React Native APIs (React Hooks, FlatList, ...) #2345

Open ferrannp opened 5 years ago

ferrannp commented 5 years ago

I created this in stackoverflow: https://stackoverflow.com/questions/55741282/flatlist-not-updating-with-react-hooks-and-realm

Basically, I created a React Hook to access Realm:

/* @flow */

import { useState, useEffect } from 'react';

export default function useRealmResultsHook(query, args): Array {
  const [data, setData] = useState(args ? query(...args) : query());

  useEffect(
    () => {
      function handleChange(newData) {
        setData(newData);
      }

      const dataQuery = args ? query(...args) : query();
      dataQuery.addListener(handleChange);
      return () => {
        dataQuery.removeAllListeners();
      };
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [query, ...args]
  );

  return data;
}

And then I use it:

const MyComponent = (props: Props) => {
  const data = useRealmResultsHook(getDataByType, [props.type]);

  return (
    <View>
      <FlatList
        data={data}
        keyExtractor={keyExtractor}
        renderItem={renderItem}
      />
    </View>
  );
};

Long story short, FlatList does not update because it looks like {{data === newData }} (reference) as Realm mutates the result (not creating a new copy).

Like this works:

setData([...newData]);

But doing so I get a plain JS array, not Realm Results anymore. What is the best way to do this? How to better clone Realm Results? I am afraid that [...newData] might be quite inefficient for big amounts of data.

TBH, it looks to be nothing about the hook but just Realm

zek commented 5 years ago

I believe we also need a sample without hooks. It is not clear how to use realmjs with react-native clearly. Examples are deprecated they must be updated

sunhak-hout commented 5 years ago

I just recently met this problem and I figured out this is the problem with React Hook but I don't know yet why it is. If you use class component and use this.setState(), this will work fine.

Anyway, I've also found the solution with React Hook.

You can try converting Realm Results into an array of Realm Object by doing so:

setData(newData.slice());

zek commented 5 years ago

@sunhak-hout but what if we have a 100k rows or something like that. I believe we need a mobx integration to realm

ferrannp commented 5 years ago

This is the workaround I ended up using:

function handleChange() {
  setData({
    data: newData,
    // Realm mutates the array instead of returning a new copy,
    // thus for a FlatList to update, we can use a timestamp as
    // extraData prop
    timestamp: Date.now(),
  });
}

And then in FlatList:

<FlatList
  ...
  extraData={timestamp}
/>

In this way, every time Realm updates, I force FlatList to update.

cybercoder commented 4 years ago

It seems pagination is a problem now with using array slice. hmmm

Divyanshmt commented 4 years ago

@ferrannp Thank you so much 🥇 💯

kraenhansen commented 4 years ago

@ferrannp @zek I've been working on an update for our "My First Ream App" example over at https://github.com/realm/my-first-realm-app/tree/kh/updated-react-native ... I've made a PR (https://github.com/realm/my-first-realm-app/pull/64) with the changes - any comments on that would be greatly appreciated!

EzeMortal commented 4 years ago

If someone want to apply the workaround from ferrannp into a component:

realm init

import RealmDatabase from 'realm'

const realm = new RealmDatabase({
   schema: [
      {name: 'Dog', properties: {name: 'string', age: 'int'}}
   ],
   schemaVersion: 2
})

query

const query = () => realm.objects('Dog')

useRealmResultsHook

function useRealmResultsHook(query, args = []) {
   const [data, setData] = useState({
      data: args ? query(...args) : query(),
      a: Date.now()
   })

   useEffect(() => {
      function handleChange(newData) {
         setData({
            data: newData,
            a: Date.now()     // the "hacky" fix, this will create a
         })                   // different object and execute a re-render
      }

      const dataQuery = args ? query(...args) : query()

      dataQuery.addListener(handleChange)

      return () => {
         dataQuery.removeAllListeners()
      }
   }, [query, ...args])

   return data.data           // this hook will return only the data from realm
}

component

const MyComponent = () => {
   const data = useRealmResultsHook(query)

   // function to show the average age of all dogs
   // this is to illustrate that the 'data' is still being a 'Results' class
   const showAge = () => {
      if (data.length > 0) {
         // we use the avg() inherited method from 'Results' class
         return data.avg('age').toFixed(2)
      }

      return 0
   }

   const handlePress = () => {
      realm.write(() => {
         // a dog live between 1 to 12 years :(
         const age = randomInt(1, 12)

         realm.create('Dog', {
            name: 'Rex',
            age: age
         })

         console.log(`inserted dog with age ${age}`)
      })
   }

   return (
      <View style={styles.container}>
         <Text style={styles.text}>{`Dogs lenght: ${data?.length}`}</Text>
         <Text style={styles.text}>{`Dogs avg age: ${showAge()}`}</Text>

         <Button title="Increment" onPress={handlePress}/>
      </View>
   )
}
shahzaibmuneeb commented 4 years ago

If someone want to apply the workaround from ferrannp into a component:

realm init

import RealmDatabase from 'realm'

const realm = new RealmDatabase({
   schema: [
      {name: 'Dog', properties: {name: 'string', age: 'int'}}
   ],
   schemaVersion: 2
})

query

const query = () => realm.objects('Dog')

useRealmResultsHook

function useRealmResultsHook(query, args = []) {
   const [data, setData] = useState({
      data: args ? query(...args) : query(),
      a: Date.now()
   })

   useEffect(() => {
      function handleChange(newData) {
         setData({
            data: newData,
            a: Date.now()     // the "hacky" fix, this will create a
         })                   // different object and execute a re-render
      }

      const dataQuery = args ? query(...args) : query()

      dataQuery.addListener(handleChange)

      return () => {
         dataQuery.removeAllListeners()
      }
   }, [query, ...args])

   return data.data           // this hook will return only the data from realm
}

component

const MyComponent = () => {
   const data = useRealmResultsHook(query)

   // function to show the average age of all dogs
   // this is to illustrate that the 'data' is still being a 'Results' class
   const showAge = () => {
      if (data.length > 0) {
         // we use the avg() inherited method from 'Results' class
         return data.avg('age').toFixed(2)
      }

      return 0
   }

   const handlePress = () => {
      realm.write(() => {
         // a dog live between 1 to 12 years :(
         const age = randomInt(1, 12)

         realm.create('Dog', {
            name: 'Rex',
            age: age
         })

         console.log(`inserted dog with age ${age}`)
      })
   }

   return (
      <View style={styles.container}>
         <Text style={styles.text}>{`Dogs lenght: ${data?.length}`}</Text>
         <Text style={styles.text}>{`Dogs avg age: ${showAge()}`}</Text>

         <Button title="Increment" onPress={handlePress}/>
      </View>
   )
}

Instead of using the hacky fix with Date.now() why not use a real updating value such as the changes?

 const handleChange = (newData, newChanges) => {
     setState({
         data: newData,
         changes: newChanges,
    });
};
EzeMortal commented 4 years ago

If someone want to apply the workaround from ferrannp into a component: realm init

import RealmDatabase from 'realm'

const realm = new RealmDatabase({
   schema: [
      {name: 'Dog', properties: {name: 'string', age: 'int'}}
   ],
   schemaVersion: 2
})

query

const query = () => realm.objects('Dog')

useRealmResultsHook

function useRealmResultsHook(query, args = []) {
   const [data, setData] = useState({
      data: args ? query(...args) : query(),
      a: Date.now()
   })

   useEffect(() => {
      function handleChange(newData) {
         setData({
            data: newData,
            a: Date.now()     // the "hacky" fix, this will create a
         })                   // different object and execute a re-render
      }

      const dataQuery = args ? query(...args) : query()

      dataQuery.addListener(handleChange)

      return () => {
         dataQuery.removeAllListeners()
      }
   }, [query, ...args])

   return data.data           // this hook will return only the data from realm
}

component

const MyComponent = () => {
   const data = useRealmResultsHook(query)

   // function to show the average age of all dogs
   // this is to illustrate that the 'data' is still being a 'Results' class
   const showAge = () => {
      if (data.length > 0) {
         // we use the avg() inherited method from 'Results' class
         return data.avg('age').toFixed(2)
      }

      return 0
   }

   const handlePress = () => {
      realm.write(() => {
         // a dog live between 1 to 12 years :(
         const age = randomInt(1, 12)

         realm.create('Dog', {
            name: 'Rex',
            age: age
         })

         console.log(`inserted dog with age ${age}`)
      })
   }

   return (
      <View style={styles.container}>
         <Text style={styles.text}>{`Dogs lenght: ${data?.length}`}</Text>
         <Text style={styles.text}>{`Dogs avg age: ${showAge()}`}</Text>

         <Button title="Increment" onPress={handlePress}/>
      </View>
   )
}

Instead of using the hacky fix with Date.now() why not use a real updating value such as the changes?

 const handleChange = (newData, newChanges) => {
     setState({
         data: newData,
         changes: newChanges,
    });
};

Cool! That's working? You tried?

shahzaibmuneeb commented 4 years ago

If someone want to apply the workaround from ferrannp into a component: realm init

import RealmDatabase from 'realm'

const realm = new RealmDatabase({
   schema: [
      {name: 'Dog', properties: {name: 'string', age: 'int'}}
   ],
   schemaVersion: 2
})

query

const query = () => realm.objects('Dog')

useRealmResultsHook

function useRealmResultsHook(query, args = []) {
   const [data, setData] = useState({
      data: args ? query(...args) : query(),
      a: Date.now()
   })

   useEffect(() => {
      function handleChange(newData) {
         setData({
            data: newData,
            a: Date.now()     // the "hacky" fix, this will create a
         })                   // different object and execute a re-render
      }

      const dataQuery = args ? query(...args) : query()

      dataQuery.addListener(handleChange)

      return () => {
         dataQuery.removeAllListeners()
      }
   }, [query, ...args])

   return data.data           // this hook will return only the data from realm
}

component

const MyComponent = () => {
   const data = useRealmResultsHook(query)

   // function to show the average age of all dogs
   // this is to illustrate that the 'data' is still being a 'Results' class
   const showAge = () => {
      if (data.length > 0) {
         // we use the avg() inherited method from 'Results' class
         return data.avg('age').toFixed(2)
      }

      return 0
   }

   const handlePress = () => {
      realm.write(() => {
         // a dog live between 1 to 12 years :(
         const age = randomInt(1, 12)

         realm.create('Dog', {
            name: 'Rex',
            age: age
         })

         console.log(`inserted dog with age ${age}`)
      })
   }

   return (
      <View style={styles.container}>
         <Text style={styles.text}>{`Dogs lenght: ${data?.length}`}</Text>
         <Text style={styles.text}>{`Dogs avg age: ${showAge()}`}</Text>

         <Button title="Increment" onPress={handlePress}/>
      </View>
   )
}

Instead of using the hacky fix with Date.now() why not use a real updating value such as the changes?

 const handleChange = (newData, newChanges) => {
     setState({
         data: newData,
         changes: newChanges,
    });
};

Cool! That's working? You tried?

Yeah! As the changes is updated every change so it re-renders the UI for me.