mst-gql

mst-gql

GraphQL 和 MobX-State-Tree 的自动化集成框架

mst-gql 是一个自动化 GraphQL 和 MobX-State-Tree 集成的开源框架。它生成类型安全的模型、查询和突变,简化了 GraphQL 与客户端状态管理的结合。该框架支持代码生成、优化更新和本地存储,为构建数据驱动的 React 应用提供了高效解决方案。mst-gql 让开发者能够更便捷地处理复杂的数据流,提高应用的可扩展性和可维护性。

mst-gqlmobx-state-treeGraphQL模型驱动ReactGithub开源项目

mst-gql

Bindings for mobx-state-tree and GraphQL

Discuss this project on spectrum

CircleCI

🚀 Installation 🚀

Installation: yarn add mobx mobx-state-tree mobx-react react react-dom mst-gql graphql-request

If you want to use graphql tags, also install: yarn add graphql graphql-tag

👩‍🎓 Why 👩‍🎓

Watch the introduction talk @ react-europe 2019: Data models all the way by Michel Weststrate

Both GraphQL and mobx-state-tree are model-first driven approaches, so they have a naturally matching architecture. If you are tired of having your data shapes defined in GraphQL, MobX-state-tree and possible TypeScript as well, this project might be a great help!

Furthermore, this project closes the gap between GraphQL and mobx-state-tree as state management solutions. GraphQL is very transport oriented, while MST is great for client side state management. GraphQL clients like apollo do support some form of client-side state, but that is still quite cumbersome compared to the full model driven power unlocked by MST, where local actions, reactive views, and MobX optimized rendering model be used.

Benefits:

  • Model oriented
  • Type reuse between GraphQL and MobX-state-tree
  • Generates types, queries, mutations and subscription code
  • Strongly typed queries, mutations, result selectors, and hooks! Auto complete all the things!
  • Local views, actions, state and model life-cycles
  • Automatic instance reuse
  • Built-in support for local storage, caching, query caching, subscriptions (over websockets), optimistic updates
  • Idiomatic store organization
  • Incremental scaffolding that preserves changes

👟 Overview & getting started 👟

The mst-gql libraries consists of two parts:

  1. Scaffolding
  2. A runtime library

The scaffolder is a compile-time utility that generates a MST store and models based on the type information provided by your endpoint. This utility doesn't just generate models for all your types, but also query, mutation and subscription code base on the data statically available.

The runtime library is configured by the scaffolder, and provides entry points to use the generated or hand-written queries, React components, and additional utilities you want to mixin to your stores.

Scaffolding

To get started, after installing mst-gql and its dependencies, the first task is to scaffold your store and runtime models based on your graphql endpoint.

To scaffold TypeScript models based on a locally running graphQL endpoint on port 4000, run: yarn mst-gql --format ts http://localhost:4000/graphql. There are several additional args that can be passed to the CLI or put in a config file. Both are detailed below.

Tip: Note that API descriptions found in the graphQL endpoint will generally end up in the generated code, so make sure to write them!

After running the scaffolder, a bunch of files will be generated in the src/models/ directory of your project (or whatever path your provided):

(Files marked ✏ can and should be edited. They won't be overwritten when you scaffold unless you use the force option.)

  • index - A barrel file that exposes all interesting things generated
  • RootStore.base - A mobx-state-tree store that acts as a graphql client. Provides the following:
    • Storage for all "root" types (see below)
    • The .query, .mutate and .subscribe low-level api's to run graphql queries
    • Generated .queryXXX ,.mutateXXX and .subscribeXXX actions based on the query definitions found in your graphQL endpoint
  • RootStore - Extends RootStore.base with any custom logic. This is the version we actually export and use.
  • ModelBase - Extends mst-gql's abstract model type with any custom logic, to be inherited by every concrete model type.
  • XXXModel.base mobx-state-tree types per type found in the graphQL endpoint. These inherit from ModelBase and expose the following things:
    • All fields will have been translated into MST equivalents
    • A xxxPrimitives query fragment, that can be used as selector to obtain all the primitive fields of an object type
    • (TypeScript only) a type that describes the runtime type of a model instance. These are useful to type parameters and react component properties
  • XXXModel - Extends XXXModdel.base with any custom logic. Again, this is the version we actually use.
  • reactUtils. This is a set of utilities to be used in React, exposing the following:
    • StoreContext: a strongly typed React context, that can be used to make the RootStore available through your app
    • useQuery: A react hook that can be used to render queries, mutations etc. It is bound to the StoreContext automatically.

The following graphQL schema will generate the store and message as shown below:

type User { id: ID name: String! avatar: String! } type Message { id: ID user: User! text: String! } type Query { messages: [Message] message(id: ID!): Message me: User } type Subscription { newMessages: Message } type Mutation { changeName(id: ID!, name: String!): User }

MessageModel.base.ts (shortened):

export const MessageModelBase = ModelBase.named("Message").props({ __typename: types.optional(types.literal("Message"), "Message"), id: types.identifier, user: types.union(types.undefined, MSTGQLRef(types.late(() => User))), text: types.union(types.undefined, types.string) })

RootStore.base.ts (shortened):

export const RootStoreBase = MSTGQLStore.named("RootStore") .props({ messages: types.optional(types.map(types.late(() => Message)), {}), users: types.optional(types.map(types.late(() => User)), {}) }) .actions((self) => ({ queryMessages( variables?: {}, resultSelector = messagePrimitives, options: QueryOptions = {} ) { // implementation omitted }, mutateChangeName( variables: { id: string; name: string }, resultSelector = userPrimitives, optimisticUpdate?: () => void ) { // implementation omitted } }))

(Yes, that is a lot of code. A lot of code that you don't have to write 😇)

Note that the mutations and queries are now strongly typed! The parameters will be type checked, and the return types of the query methods are correct. Nonetheless, you will often write wrapper methods around those generated actions, to, for example, define the fragments of the result set that should be retrieved.

Initializing the store

To prepare your app to use the RootStore, it needs to be initialized, which is pretty straight forward, so here is quick example of what an entry file might look like:

// 1 import React from "react" import * as ReactDOM from "react-dom" import "./index.css" import { App } from "./components/App" // 2 import { createHttpClient } from "mst-gql" import { RootStore, StoreContext } from "./models" // 3 const rootStore = RootStore.create(undefined, { gqlHttpClient: createHttpClient("http://localhost:4000/graphql") }) // 4 ReactDOM.render( <StoreContext.Provider value={rootStore}> <App /> </StoreContext.Provider>, document.getElementById("root") ) // 5 window.store = rootStore
  1. Typical react stuff, pretty unrelated to this library
  2. Bunch of imports that are related to this lib :)
  3. When starting our client, we initialize a rootStore, which, in typical MST fashion, takes 2 arguments:
    1. The snapshot with the initial state of the client. In this case it is undefined, but one could rehydrate server state here, or pick a snapshot from localStorage, etc.
    2. The transportation of the store. Either gqlHttpClient, gqlWsClient or both need to be provided.
  4. We initialize rendering. Note that we use StoreContext.Provider to make the store available to the rest of the rendering three.
  5. We expose the store on window. This has no practical use, and should be done only in DEV builds. It is a really convenient way to quickly inspect the store, or even fire actions or queries directly from the console of the browser's developer tools. (See this talk for some cool benefits of that)

Loading and rendering your first data

Now, we are ready to write our first React components that use the store! Because the store is a normal MST store, like usual, observer based components can be used to render the contents of the store.

However, mst-gql also provides the useQuery hook that can be used to track the state of an ongoing query or mutation. It can be used in many different ways (see the details below), but here is a quick example:

import React from "react" import { observer } from "mobx-react" import { Error, Loading, Message } from "./" import { useQuery } from "../models/reactUtils" export const Home = observer(() => { const { store, error, loading, data } = useQuery((store) => store.queryMessages() ) if (error) return <Error>{error.message}</Error> if (loading) return <Loading /> return ( <ul> {data.messages.map((message) => ( <Message key={message.id} message={message} /> ))} </ul> ) })

Important: useQuery should always be used in combination with observer from the "mobx-react" or "mobx-react-lite" package! Without that, the component will not re-render automatically!

The useQuery hook is imported from the generated reactUtils, and is bound automatically to the right store context. The first parameter, query, accepts many different types of arguments, but the most convenient one is to give it a callback that invokes one of the query (or your own) methods on the store. The Query object returned from that action will be used to automatically update the rendering. It will also be typed correctly when used in this form.

The useQuery hook component returns, among other things, the store, loading and data fields.

If you just need access to the store, the useContext hook can be used: useContext(StoreContext). The StoreContext can be imported from reactUtils as well.

Mutations

Mutations work very similarly to queries. To render a mutation, the useQuery hook can be used again. Except, this time we start without an initial query parameter. We only set it once a mutation is started. For example the following component uses a custom toggle action that wraps a graphQL mutation:

import * as React from "react" import { observer } from "mobx-react" import { useQuery } from "../models/reactUtils" export const Todo = observer(({ todo }) => { const { setQuery, loading, error } = useQuery() return ( <li onClick={() => setQuery(todo.toggle())}> <p className={`${todo.complete ? "strikethrough" : ""}`}>{todo.text}</p> {error && <span>Failed to update: {error}</span>} {loading && <span>(updating)</span>} </li> ) })

Optimistic updates

The Todo model used in the above component is defined as follows:

export const TodoModel = TodoModelBase.actions((self) => ({ toggle() { return self.store.mutateToggleTodo({ id: self.id }, undefined, () => { self.complete = !self.complete }) } }))

There are few things to notice:

  1. Our toggle action wraps around the generated mutateToggleTodo mutation of the base model, giving us a much more convenient client api.
  2. The Query object created by mutateToggleTodo is returned from our action, so that we can pass it (for example) to the setQuery as done in the previous listing.
  3. We've set the third argument of the mutation, called optimisticUpdate. This function is executed immediately when the mutation is created, without awaiting it's result. So that the change becomes immediately visible in the UI. However, MST will record the patches. If the mutation fails in the future, any changes made inside this optimisticUpdate callback will automatically be rolled back by reverse applying the recorded patches!

Customizing the query result

Mutations and queries take as second argument a result selector, which defines which objects we want to receive back from the backend. Our mutateToggleTodo above leaves it to undefined, which defaults to querying all the shallow, primitive fields of the object (including __typename and id).

However, in the case of toggling a Todo, this is actually overfetching, as we know the text won't change by the mutation. So instead we can provide a selector to indicate that we we are only interested in the complete property: "__typename id complete". Note that we have to include __typename and id so that mst-gql knows to which object the result should be applied!

Children can be retrieved as well by specifying them explicitly in the result selector, for example: "__typename id complete assignee { __typename id name }. Note that for children __typename and id (if applicable) should be selected as well!

It is possible to use gql from the graphql-tag package. This enables highlighting in some IDEs, and potentially enables static analysis.

However, the recommended way to write the result selectors is to use the query builder that mst-gql will generate for you. This querybuilder is entirely strongly typed, provides auto completion and automatically takes care of __typename and id fields. It can be used by passing a function as second argument to a mutation or query. That callback will be invoked with a querybuilder for the type of object that is returned. With the querybuilder, we could write the above mutation as:

export const TodoModel = TodoModelBase.actions((self) => ({ toggle() { return self.store.mutateToggleTodo({ id: self.id }, (todo) => todo.complete) } }))

To select multiple fields, simply keep "dotting", as the query is a fluent interface. For example: user => user.firstname.lastname.avatar selects 3 fields.

Complex children can be selected by calling the field as function, and provide a callback to that field function (which in turn is again a query builder for the appropriate type). So the following example selector selects the timestamp and text of a message. The name and avatar inside the user property, and finally also the likes properties. For the likes no further subselector was specified, which means that only __typename and id will be retrieved.

// prettier-ignore msg => msg .timestamp .text .user(user => user.name.avatar) .likes() .toString()

To create reusable query fragments, instead the following syntax can be used:

import { selectFromMessage } from "./MessageModel.base" // prettier-ignore export const MESSAGE_FRAGMENT = selectFromMessage() .timestamp .text .user(user => user.name.avatar) .likes() .toString()

Customizing generated files

You can customize all of the defined mst types: RootStore, ModelBase, and every XXXModel.

However, some files (including but not limited to .base files) should not be touched, as they probably need to be scaffolded again in the future.

Thanks to how MST models compose, this means that you can introduce as many additional views, actions and props as you want to your models, by chaining more calls unto the model definitions. Those actions will often wrap around the generated methods, setting some predefined parameters, or composing the queries into bigger operations.

Example of a generated model, that introduces a toggle action that wraps around one of the generated mutations:

// src/models/TodoModel.js import { TodoModelBase } from "./TodoModel.base" export const TodoModel = TodoModelBase.actions((self) => ({ toggle() { return self.store.mutateToggleTodo({ id: self.id }) } }))

That's it for the introduction! For the many different ways in which the above can applied in practice, check out the examples

Server side rendering with react

There is an exported function called getDataFromTree which you can use to preload all queries, note that you must set ssr: true as an option in order for this to work

async function preload() { const client = RootStore.create(undefined, { gqlHttpClient: createHttpClient("http://localhost:4000/graphql"), ssr: true }) const html = await getDataFromTree(<App client={client} />, client) const initalState = getSnapshot(client) return [html,

编辑推荐精选

蛙蛙写作

蛙蛙写作

AI小说写作助手,一站式润色、改写、扩写

蛙蛙写作—国内先进的AI写作平台,涵盖小说、学术、社交媒体等多场景。提供续写、改写、润色等功能,助力创作者高效优化写作流程。界面简洁,功能全面,适合各类写作者提升内容品质和工作效率。

AI助手AI工具AI写作工具AI辅助写作蛙蛙写作学术助手办公助手营销助手
Trae

Trae

字节跳动发布的AI编程神器IDE

Trae是一种自适应的集成开发环境(IDE),通过自动化和多元协作改变开发流程。利用Trae,团队能够更快速、精确地编写和部署代码,从而提高编程效率和项目交付速度。Trae具备上下文感知和代码自动完成功能,是提升开发效率的理想工具。

热门AI工具生产力协作转型TraeAI IDE
问小白

问小白

全能AI智能助手,随时解答生活与工作的多样问题

问小白,由元石科技研发的AI智能助手,快速准确地解答各种生活和工作问题,包括但不限于搜索、规划和社交互动,帮助用户在日常生活中提高效率,轻松管理个人事务。

聊天机器人AI助手热门AI工具AI对话
Transly

Transly

实时语音翻译/同声传译工具

Transly是一个多场景的AI大语言模型驱动的同声传译、专业翻译助手,它拥有超精准的音频识别翻译能力,几乎零延迟的使用体验和支持多国语言可以让你带它走遍全球,无论你是留学生、商务人士、韩剧美剧爱好者,还是出国游玩、多国会议、跨国追星等等,都可以满足你所有需要同传的场景需求,线上线下通用,扫除语言障碍,让全世界的语言交流不再有国界。

讯飞智文

讯飞智文

一键生成PPT和Word,让学习生活更轻松

讯飞智文是一个利用 AI 技术的项目,能够帮助用户生成 PPT 以及各类文档。无论是商业领域的市场分析报告、年度目标制定,还是学生群体的职业生涯规划、实习避坑指南,亦或是活动策划、旅游攻略等内容,它都能提供支持,帮助用户精准表达,轻松呈现各种信息。

热门AI工具AI办公办公工具讯飞智文AI在线生成PPTAI撰写助手多语种文档生成AI自动配图
讯飞星火

讯飞星火

深度推理能力全新升级,全面对标OpenAI o1

科大讯飞的星火大模型,支持语言理解、知识问答和文本创作等多功能,适用于多种文件和业务场景,提升办公和日常生活的效率。讯飞星火是一个提供丰富智能服务的平台,涵盖科技资讯、图像创作、写作辅助、编程解答、科研文献解读等功能,能为不同需求的用户提供便捷高效的帮助,助力用户轻松获取信息、解决问题,满足多样化使用场景。

模型训练热门AI工具内容创作智能问答AI开发讯飞星火大模型多语种支持智慧生活
Spark-TTS

Spark-TTS

一种基于大语言模型的高效单流解耦语音令牌文本到语音合成模型

Spark-TTS 是一个基于 PyTorch 的开源文本到语音合成项目,由多个知名机构联合参与。该项目提供了高效的 LLM(大语言模型)驱动的语音合成方案,支持语音克隆和语音创建功能,可通过命令行界面(CLI)和 Web UI 两种方式使用。用户可以根据需求调整语音的性别、音高、速度等参数,生成高质量的语音。该项目适用于多种场景,如有声读物制作、智能语音助手开发等。

咔片PPT

咔片PPT

AI助力,做PPT更简单!

咔片是一款轻量化在线演示设计工具,借助 AI 技术,实现从内容生成到智能设计的一站式 PPT 制作服务。支持多种文档格式导入生成 PPT,提供海量模板、智能美化、素材替换等功能,适用于销售、教师、学生等各类人群,能高效制作出高品质 PPT,满足不同场景演示需求。

讯飞绘文

讯飞绘文

选题、配图、成文,一站式创作,让内容运营更高效

讯飞绘文,一个AI集成平台,支持写作、选题、配图、排版和发布。高效生成适用于各类媒体的定制内容,加速品牌传播,提升内容营销效果。

AI助手热门AI工具AI创作AI辅助写作讯飞绘文内容运营个性化文章多平台分发
材料星

材料星

专业的AI公文写作平台,公文写作神器

AI 材料星,专业的 AI 公文写作辅助平台,为体制内工作人员提供高效的公文写作解决方案。拥有海量公文文库、9 大核心 AI 功能,支持 30 + 文稿类型生成,助力快速完成领导讲话、工作总结、述职报告等材料,提升办公效率,是体制打工人的得力写作神器。

下拉加载更多