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,

编辑推荐精选

Refly.AI

Refly.AI

最适合小白的AI自动化工作流平台

无需编码,轻松生成可复用、可变现的AI自动化工作流

酷表ChatExcel

酷表ChatExcel

大模型驱动的Excel数据处理工具

基于大模型交互的表格处理系统,允许用户通过对话方式完成数据整理和可视化分析。系统采用机器学习算法解析用户指令,自动执行排序、公式计算和数据透视等操作,支持多种文件格式导入导出。数据处理响应速度保持在0.8秒以内,支持超过100万行数据的即时分析。

AI工具酷表ChatExcelAI智能客服AI营销产品使用教程
TRAE编程

TRAE编程

AI辅助编程,代码自动修复

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

AI工具TraeAI IDE协作生产力转型热门
AIWritePaper论文写作

AIWritePaper论文写作

AI论文写作指导平台

AIWritePaper论文写作是一站式AI论文写作辅助工具,简化了选题、文献检索至论文撰写的整个过程。通过简单设定,平台可快速生成高质量论文大纲和全文,配合图表、参考文献等一应俱全,同时提供开题报告和答辩PPT等增值服务,保障数据安全,有效提升写作效率和论文质量。

AI辅助写作AI工具AI论文工具论文写作智能生成大纲数据安全AI助手热门
博思AIPPT

博思AIPPT

AI一键生成PPT,就用博思AIPPT!

博思AIPPT,新一代的AI生成PPT平台,支持智能生成PPT、AI美化PPT、文本&链接生成PPT、导入Word/PDF/Markdown文档生成PPT等,内置海量精美PPT模板,涵盖商务、教育、科技等不同风格,同时针对每个页面提供多种版式,一键自适应切换,完美适配各种办公场景。

AI办公办公工具AI工具博思AIPPTAI生成PPT智能排版海量精品模板AI创作热门
潮际好麦

潮际好麦

AI赋能电商视觉革命,一站式智能商拍平台

潮际好麦深耕服装行业,是国内AI试衣效果最好的软件。使用先进AIGC能力为电商卖家批量提供优质的、低成本的商拍图。合作品牌有Shein、Lazada、安踏、百丽等65个国内外头部品牌,以及国内10万+淘宝、天猫、京东等主流平台的品牌商家,为卖家节省将近85%的出图成本,提升约3倍出图效率,让品牌能够快速上架。

iTerms

iTerms

企业专属的AI法律顾问

iTerms是法大大集团旗下法律子品牌,基于最先进的大语言模型(LLM)、专业的法律知识库和强大的智能体架构,帮助企业扫清合规障碍,筑牢风控防线,成为您企业专属的AI法律顾问。

SimilarWeb流量提升

SimilarWeb流量提升

稳定高效的流量提升解决方案,助力品牌曝光

稳定高效的流量提升解决方案,助力品牌曝光

Sora2视频免费生成

Sora2视频免费生成

最新版Sora2模型免费使用,一键生成无水印视频

最新版Sora2模型免费使用,一键生成无水印视频

Transly

Transly

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

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

下拉加载更多