Why we decided against GraphQL for local state management

序言和动机
Here at OkCupid, we’re pretty big fans of usingGraphQl。当涉及到我们的任何客户端平台上获取数据时,查询语言提供的抽象为我们提供了灵活性,使我们可以精确获取每种情况下所需的数据。
At the end of the day, GraphQL really is just that: an abstraction. The mutation, query, and subscription types abstractly model the fundamental ways in which we interact with any data. The schema serves as a contract between some source of data and its destination, and it defines what data can be queried and how it ought to be queried. The data that an incoming query outlines would be resolved by our GraphQL server instance in most cases, but the destination of that data (in our case, let’s say it’s a mobile app or web app acting as a client), doesn’t really need to know about the source or resolution strategy of the data in question.
这真的很好,因为这意味着数据可以来自GraphQL Server可以访问的任何地方。也许我们想使用文件系统中的某些内容或本地数据库或远程数据库来解决我们的数据。也许我们真的可以通过RPC或REST或任何协议致电其他服务器。也许我们的数据目前在某个地方的内存中,从技术上讲也很好!我们的数据源的冷漠是允许此模型的原因,并且数据图的体系结构是如此可扩展(Mandi Wise具有一个很棒的视频,可以证明这一点。涵盖联合图的概念)。
Regardless of the source of the data, the client implementation doesn’t really need to change at all, and that’s至关重要的对于理解将GraphQL用于本地状态管理的概念很重要。想象一下您的应用程序的本地状态:毕竟,这确实只是数据的另一个来源,不是吗?因此,问题是,是什么阻止我们利用此查询语言范式提供给我们的抽象来管理此数据?
这个怎么运作
好吧,答案真的没有。如果您将状态存储在应用程序中的某个地方,则可以从理论上说明GraphQl查询使用该状态数据。来自Apollo(我们认为,GQL的所有事实提供者)我们尝试使用GraphQL指令来表示应以这种方式解决哪些给定查询的部分:@client
。
对于外观的示例,让我们想象我们想拥有一个应用程序,用户可以在其中彼此发送消息。为了获取有关用户已发出消息的信息,我们可能会向我们的服务器进行查询。但是,也许我们想知道用户当前是否对其任何给定的对话打开了消息窗口。该服务器实际上并不关心每个对话的信息,但是我们的前端应用程序非常关心它,因此很有意义,我们可能会选择将类似的内容存储在客户端状态中。假设是这样,然后可以为该数据构建合理的查询,包括本地状态,例如:
查询getallMessages($ userId:id!){
用户(ID:$ userId){
姓名
profilePic
messages {
ID
通讯{
姓名
profilePic
}
开了@client
}
}
}
在此示例中,我们的开了
每个用户的消息的标志可以存储在我们客户的状态中,因为这并不是一个后端的问题。但是,其余数据可以从我们的服务器中获取,没有其他需要更改。将我们的数据源(客户端与服务器)与单个查询混合的能力是一个非常有力的想法,并且可以导致一些非常灵活的单个查询。
Due to thestrategy解决这个问题@client
directive being a traversal, it means that this directive can recursively apply across parent-child and neighbor-neighbor relationships in our data, allowing us to achieve the same experience of a data graph, except with our client state. Our client state can have direct access to pieces of its parent (non-client-state) structures, and it can be something as simple as scalar Fields like boolean flags, or even data that is more deeply related and structured.
客户端状态也不需要与某些服务器数据相关!它实际上可能是我们可能想要存储为客户端状态的任何数据,例如用户当前的主题偏好设置,完全关注的前端关注,但仍然有一种状态。不过,这些天黑暗模式是如此,对吗?
那么,像这样的事情如何工作?好吧,在引擎盖下,我们需要在我们的ApolloClient
instance in order for it to have a strategy for resolving client-side queries. To do so, we explicitly spell this out by adding some new resolvers to our client, just like we would write to resolve queries on our server instance. TheApolloClient
instance can add resolvers both upon initialization, as well as on an ad-hoc basis usingClient.Addresolver(SOMENEWRESOLVERTOADD)
。Apollo defines the function signature that handles resolution like so, which ought to look very familiar if you've worked withapollo-server
在过去:
type ResolverFn = (
父母:任何,
args:任何,
{cache}:{cache:apollocache }
)=> any;
忽略父节点和参数参数,我们看到我们破坏了一个属性,称为缓存
, much like we destructure adataSources
属性apollo-server
与此功能类型平行。这是因为在这种情况下,我们的缓存负责我们的客户端世界中的数据源。让我们看看在这种情况下,客户端的解析器设置将是什么样子:
const defaultresolver = {
询问: {
用户:{
消息:{
开了: (parent, args, { cache }) => {
// reference the cache to get your data return
cache.readquery({
查询:消息_is_open_query,
变量:{
messageId:parent.id,
},,
});
},,
},,
},,
},,
};
After that, we’ll also want to provide the cache with some initial state too, so that our first cache read will resolve, so we’ll want to define that as well and feed it to our cache upon initialization.
//定义初始客户端状态
const defaultState = {
用户:{
消息:{
等法:false,
}
}
}const client = new ApolloClient({
//其他Apollo配置,例如链接
//和缓存定义
resolvers: defaultResolver,
});//用您的初始状态加油
client.writedata({
查询:消息_is_open_query,
数据:{defaultstate,},
});
In this example, we’re just setting the default state for a given message
阿波罗所拥有的直觉不仅是提供此指令和解决客户端的查询的选项,而且还提供了最多的客户端缓存ApolloClient
instances define anyway (which is normally used to store responses for our queries that do actually resolve on the server) as a location to store this client state. This makes a lot of sense, we have a local store (ourApolloClient
的缓存),我们有一种与该商店互动的方法(使用GQL)...从本质上讲,这是一种解决方案Redux(我确定此时不需要介绍)或MobXoffers us right?
Well, yes. It works perfectly fine, too! As we explored this as an option however, we noticed a few things that eventually led us to make the decision to not rely on Apollo for state management.
The issues we encountered
那么为什么我们决定对实现这个n? Well, the reasoning behind the decision is certainly specific to our scenario and maybe won’t be as important to others, but does contain some insights that I feel will be considerations that anyone going down the Apollo path will have to weigh as well.
开发人员的开销和学习曲线
尽管这是舞台管理解决方案,但它带有一些新的开销。现在将必须为其客户端状态编写解析器,尽管情况就是如此anystate management option, it is not as simple as it may seem.
为了真正正确编写这些解析器,您将需要直接使用缓存的经验 /才能。从3.0版开始阿波罗/客户端
has had some高速缓存如何处理标准化和非归一化数据的更新更新。了解缓存的使用方式ID
沙__类型
s, deciding whether to merge or replace data, and learning how to do so are all par for the course with this paradigm.Sidenote,Khalil @ Apollo最近出版了一篇令人难以置信的博客文章深入了解阿波罗缓存并了解缓存归一化。这为我们提供了我们缓存数据的两个选项之一:确保每个查询请求都是我们请求的数据的独特识别字段(这不是失败了请求我们想要的任何字段的目的吗?)或显式编写typePolicies
告诉我们的缓存如何使我们的数据正常化。从编写客户端库的人的角度来看,该库必须在许多不同的用例中正常工作,我了解这样的解决方案的动机。但是,这不是通过Redux等解决方案的客户端状态的问题。
Contrasting this paradigm against something likeReact的上下文API与用户挂钩, or even the Redux architecture, it seems like the Apollo solution is more to understand and manage from a developer perspective. For that tradeoff, though, we do gain the ability to think about and interact with我们所有应用程序的数据以相同的方式,这无疑是一个很棒的好处。但是这值得吗?
新事物,借了什么?
好吧,我们已经在OKC上与Redux合作,即使有回流,我们也已经在一些旧代码的示例中合作。在我们的应用程序中添加一个新的国家管理选项确实会导致混乱的混乱,这对于那些开始登上我们的团队以首先将其缠住的人来说,这将是复杂的。就个人而言,我认为开发人员的经验和可维护性应该是任何建筑或框架决定的定义因素。
“ redux is dead”论点已经提出了很多次,与之相关的传统成本以吨of boilerplate and wrapping components can easily be argued to not be very scalable, as one has to make changes to a handful of files before getting anywhere. Despite this, though, it definitely has matured over the years. If done right, it can definitely be a breeze to work with and it clearly does have some staying power (not to mention, working with the Redux hooks is actually really nice). Not to mention, ripping out our existing architecture would take months, and adding another paradigm to learn and follow alongside the existing state management paradigms would cause more context switching when devs are writing code, and would probably just confuse and burden them more.
最重要的是,对于使用和理解Redux(或任何成熟的OSS),也有无数的资源可供使用,这是我在研究Apollo客户端国家管理时肯定有问题的一件事;与Redux这样的东西相比,关于阿波罗方法的文档,视频和文章的数量不多,也许是因为它在生命的早期更加晦涩。同样,更成熟的解决方案可能会在定义稳定的API的术语中提供更多的价值,而开发人员可以与之合作,而在这方面,年轻的API可能会更加动荡(但是,这都是非常有点的,我承认)。
However, the common argument against Redux of having to “change so many files” and “it being overly complicated” doesn’t really seem remedied by Apollo’s solution. We’d still want to sensibly colocate the definition of resolvers and initial state, and after having done something like that myself, I felt like I was just writing the boilerplate for some Redux which felt pretty ironic to me. Working with the cache directly doesn’t seem less complicated to me than working with the store, either.
Apollo also has a Dev Tools offering as well, which I really liked using and found to be useful as well, but it too feels a tad immature when put up against something like Redux’s parallel. Sometimes, it doesn’t want to launch. It doesn’t offer interesting features like Time Travel, and the one thing I was excited about using it for, which was the client-side introspection, would have required definingTypedefs
在ApolloClient
实例(这就是创造的过程ing a schema for our client-side state, which is really just another whole set of things to worry about and manage, but I'll admit that perhaps Typescript or codegen could really shine in this scenario). In other libraries I've used, there isn't a need to define the shape of our client-side state multiple times, and if anything, it's starting to seem like some Apollo-side boilerplate that is starting to accumulate.
Other options
我们还一直在尝试React的上下文API,并且大概也希望将来考虑其他一些选择。但是,考虑到我们选择对捆绑尺寸的影响的考虑对我们也非常重要。上下文/阿波罗可以完全消除对状态管理依赖性的需求吗?对于一些更简单的应用程序,我认为其中有示例,这些示例已被证明足够了。同样,也有一些示例Apollo的解决方案也足够了!还有MOBX,Facebook的新开源产品畏缩,甚至使用与状态机进行建模XState。
Where do we net out?
很难不认识阿波罗所做的令人难以置信的工作。Apollo和GraphQl做出了奇迹,以清理我们的API和客户的一般网络层。Pursuing it as an option for state management, however, has major implications for our codebase, and at this time we just didn’t feel the argument was compelling enough given the maturity of Apollo’s client-side state management and the current setup of our client-side state. The return-on-investment of implementing something new in a codebase with a lot of existing architecture needs to be relatively high in my opinion. I’m just not sure that the return on investment with Apollo is high enough in this case.
Armed with some new insights after having explored Apollo’s offering some more, though, we will hopefully be able to come to a better conclusion eventually about what we should rely on to solve the problem, and how we should think about the approach, architecture, and trade-offs of any library, framework, or tool we decide to roll with. Until then, we’ll continue to keep on our eyes on how Apollo’s client state solution evolves.
Originally published athttps://tech.188bet金宝搏官网okcupid.com2020年8月20日。