mst-query #1726
-
| I've written a query library for mobx-state-tree: https://github.com/ConrabOpto/mst-query/ It's sort of react-query, but with reactive models! Another inspiration is mst-gql. The main differences is that mst-query supports automatic garbage collection, and can be used with any backend. Basic query exampleimport { flow, types } from 'mobx-state-tree';
import { createQuery, MstQueryRef } from 'mst-query';
const UserModel = types.model('UserModel', {
    id: types.identifier,
    name: types.string,
    age: types.number,
});
const MessageModel = types.model('MessageModel', {
    id: types.identifier,
    message: types.string,
    created: types.Date,
    createdBy: MstQueryRef(UserModel),
});
const getItem = ({ id }) => {
    return fetch('...').then((res) => res.json());
};
const MessageQuery = createQuery('MessageQuery', {
    data: MstQueryRef(MessageModel),
    request: types.model({ id: types.string }),
    env: types.frozen(),
}).actions((self) => ({
    run: flow(function* () {
        const next = yield* self.query(getItem, { id: self.request.id });
        const { data, result, error } = next<typeof MessageQuery>();
    }),
}));import { useQuery } from 'mst-query';
import { observer } from 'mobx-react';
import { MessageQuery } from './MessageQuery';
const MesssageView = observer((props) => {
    const { id } = props;
    const { data, error, isLoading } = useQuery(MessageQuery, {
        request: { id },
        cacheMaxAge: 300 // cache for 5 minutes
    });
    if (error) {
        return <div>An error occured...</div>;
    }
    if (isLoading) {
        return <div>Loading...</div>;
    }
    return <div>{data.message}</div>;
});Mutationimport { types } from 'mobx-state-tree';
import { createMutation } from 'mst-query';
import { MessageModel } from './models';
import { addMessage } from './api';
const AddMessageMutation = createMutation('AddMessage', {
    data: MstQueryRef(MessageModel),
    request: types.model({ message: types.string, userId: types.number }),
    env: types.frozen(),
})
    .views((self) => ({
        get canRun() {
            return !self.isLoading && self.request.message.length > 0;
        },
    }))
    .actions((self) => ({
        run: flow(function* () {            
            const next = yield* self.mutate(addMessage, self.request);
            const { data } = next<typeof AddMessageMutation>();
            // add new message to query
            const messageList = queryCache.find(MessageListQuery);
            messageList?.addMessage(data);
            self.reset(); // restore request model to initial state
        }),
        setMessage(message: string) {
            self.request.message = message;
        },
    }));import { useMutation } from 'mst-query';
import { observer } from 'mobx-react';
import { AddMessageMutation } from './AddMessageMutation';
const AddMessage = observer((props) => {
    const [addMessage, { mutation } = useMutation(AddMessageMutation, {
        request: { message: '', userId: 1 },
    });
    return (
        <div>
            <textarea
                value={mutation.request.message}
                onChange={ev => mutation.setMessage(ev.target.value)} />
            <button
                type="button"
                disabled={!mutation.canRun}
                onClick={() => addMessage()}>Send</button>
        </div>
    );
}); | 
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 1 reply
-
| This is great, @k-ode! Very cool -- we need more support for GraphQL in MST, and this is a great option. Feel free to add a link to your library in the documentation via a PR. | 
Beta Was this translation helpful? Give feedback.
-
| Just a quick update. It's now possible to use a custom root store with mst-query. This means that it's easier to use with setups more commonly used with mobx-state-tree. Here's an example: import { types, IAnyModelType, destroy } from 'mobx-state-tree';
import { ItemModel } from './ItemModel';
import { ListModel } from './ListModel';
import { UserModel } from './UserModel';
const ItemStore = types
    .model({
        items: types.map(ItemModel),
    })
    .actions((self) => ({
        put(instance) {
            self.items.put(instance);
        },
        get(id: string) {
            return self.items.get(id);
        },
        delete(id: string) {
            self.items.delete(id);
        },
    }));
const UserStore = types
    .model({
        users: types.map(UserModel),
    })
    .actions((self) => ({
        put(instance) {
            self.users.put(instance);
        },
        get(id: string) {
            return self.users.get(id);
        },
        delete(id: string) {
            self.users.delete(id);
        },
    }));
const ListStore = types
    .model({
        lists: types.map(ListModel),
    })
    .actions((self) => ({
        put(instance) {
            self.lists.put(instance);
        },
        get(id: string) {
            return self.lists.get(id);
        },
        delete(id: string) {
            self.lists.delete(id);
        },
    }));
const getStoreName = (typeName: string) => {
    return `${typeName.replace(/Model$/, '').toLowerCase()}Store` ;
};
export const RootStore = types
    .model('RootStore', {
        itemStore: types.optional(ItemStore, {}),
        userStore: types.optional(UserStore, {}),
        listStore: types.optional(ListStore, {}),
    })
    .views((self) => ({
        get models() {
            /* This property is used for enumerating all models for garbage collection. 
               Excluding items from this Map is an easy way to make sure they never get gc:ed */
            return new Map([
                ...(self.itemStore.items as any),
                ...self.userStore.users,
                ...self.listStore.lists,
            ]); 
        },
    }))
    .actions((self) => ({
       /* put, get and delete are required actions */
        put(type: IAnyModelType, _id: string, instance: any) {
            self[getStoreName(type.name)].put(instance);
        },
        get(type: IAnyModelType, id: string) {
            return self[getStoreName(type.name)].get(id);
        },
        delete(type: IAnyModelType, id: string, instance: any) {
            self[getStoreName(type.name)].delete(id);
            destroy(instance);
        },
    }));
// In your entry file...
import { configure } from 'mst-query';
const env = {};
configure({ env, rootStore: RootStore.create({}, env) }); | 
Beta Was this translation helpful? Give feedback.
-
| We've now used  Creating and using a query is now a lot simpler. import { createQuery, createModelStore, createRootStore } from 'mst-query';
import { MessageModel } from './models';
const MessageListQuery = createQuery('MessageQuery', {
    data: types.array(types.reference(MessageModel)),
    request: types.model({ filter: '' }),
    endpoint({ request }) {
        return fetch(`api/messages?filter=${request.filter}`)
            .then((res) => res.json());
    },
});
const MessageStore = createModelStore('MessageStore', MessageModel).props({
    messageListQuery: types.optional(MessageListQuery, {}),
});
const RootStore = createRootStore({
    messageStore: types.optional(MessageStore, {}),
});
const queryClient = new QueryClient({ RootStore });
const { QueryClientProvider, useRootStore } = createContext(queryClient);
const MesssageList = observer((props) => {
    const { messageStore } = useRootStore();
    const [filter, setFilter] = useState('');  
    const { data, error, isLoading } = useQuery(messageStore.messageListQuery, {
        request: { filter },
    });
    if (!data) return null;
    return <div>{data.map(message => <div>{message.text}</div>)}</div>;
});
const App = () => {
    return (
        <QueryClientProvider>
            <MessageList />
        </QueryClientProvider>
    );
}My next focus will be on improving docs so feel free to drop an issue if something is unclear or difficult. | 
Beta Was this translation helpful? Give feedback.
This is great, @k-ode! Very cool -- we need more support for GraphQL in MST, and this is a great option. Feel free to add a link to your library in the documentation via a PR.