realm / realm-js

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

`@realm/react` create class-aware hooks #6284

Open kraenhansen opened 10 months ago

kraenhansen commented 10 months ago

Problem

Users want the simples way possible to query their data, but the current useObject and useQuery APIs take the type as an argument, which could be simplified further.

Additionally, the current 3 positional argument layout of useQuery means users cannot rely on eslint rules to check for missing deps (see https://github.com/realm/realm-js/issues/6259).

Solution

A possible solution to both issues above, is to expose functions to generate class / type -aware hooks, which wouldn't require users to pass the type argument:

One example wrapping useQuery (proposed by @kraenhansen)

export const usePersons = createQueryHook<Person>("Person");
export const useDogs = createQueryHook<Dog>("Dog");
// ... and so on, one for every class in their schema

// `createQueryHook` could even have an overload for class-based models, like some of our other APIs:
export const usePersons = createQueryHook(Person);

// And in a consuming component:
const twenty = 20;
const teenagers = usePersons(persons => persons.filtered("age < $0", twenty), [twenty]);

Another example handling both useQuery and useObject (proposed by @takameyer)

import {createModelHooks} from '@realm/react';

// or
import {createModelHooks} from '../MyRealmContext';

class Person extend Realm.Object{} //some class based model

const [usePersonById, usePerson] = createModelHooks(Person);

A third example where the user just have to pass model classes and the hooks are generated for them (proposed by @bimusiek)

enum SchemaName {
    Dog = 'Dog',
    Cat = 'Cat'
}

abstract class RealmModel {
    static schemaName: SchemaName;

    static get() {
        console.log('get');
    }
}

class Dog extends RealmModel {
    static schemaName = SchemaName.Dog

    static woof() {
        console.log('woof');
    }
}

class Cat extends RealmModel {
    static schemaName = SchemaName.Cat

    static meow() {
        console.log('meow')
    }
}

type SchemaWithModels = {
    [SchemaName.Cat]: typeof Cat;
    [SchemaName.Dog]: typeof Dog
};

function createReactUtilities<Model extends typeof RealmModel>(models: Model[]) {
    type SchemaNameHooks = {
        [K in SchemaName as K extends string ? `use${K}` : never]:
        (callback: (query: SchemaWithModels[K]) => void, deps: any[]) => void
    }

    return models.reduce((hooks, model) => {
        return {
            ...hooks,
            [`use${model.schemaName}`]: (callback: any) => {
                callback(model);
            }
        }
    }, {}) as SchemaNameHooks;
}

const utils = createReactUtilities([Dog, Cat]);
utils.useCat((query) => {
    query.get();
    query.meow();
}, [])
utils.useDog((query) => {
    query.get();
    query.woof();
}, [])

Alternatives

Document and add to examples how users could use bind to create derived hooks:

const usePersons = useQuery.bind(null, Person);

How important is this improvement for you?

I would like to have it but have a workaround

Feature would mainly be used with

Atlas Device Sync

bimusiek commented 2 months ago

I was just testing new way of passing an object with type & query as first argument, however it does not work as expected. The rules requires an inline function, not an object with a function inside.

CleanShot 2024-07-26 at 13 31 22@2x

kraenhansen commented 2 months ago

Argh 😞 I don't know how we missed that. This just makes it even more compelling to provide a "class-aware" hook.

bimusiek commented 2 months ago

The class-aware hook would the best, yes. However if you are introducing already new way of using useQuery, why not to allow to pass function as first argument for now?

Also, due to the way our Realm integration is built, we need to have access to createUseQuery where we pass our own useRealm. Example in PR: https://github.com/realm/realm-js/pull/6819