react-brain
Browse decisions
React & language foundations 3App architecture 11UI 13Platform & native 9Build, test, observe, secure 6AI in React apps 3stack composerdecision recordscensusstaleness benchnewsletterarchitecturechangelogmethodologyroadmap

entriesapp-architecture · verified 2026-10-08 · react + react-native

Client & server state management

reviewedconfidence: hightrack: heldthis tier holds 83% on the public scorecard (6/11 graded · 0 overturned) →

related decisions: data · forms · nav

cited by: react-core · data · forms

re-verified 6× — 2026-10-08 · 2026-09-24 · 2026-09-10 · 2026-08-04 · 2026-07-28 · 2026-07-13 · changelog

recommendation

TanStack Query for server state + Zustand for client/UI state (React and React Native). They solve different problems and compose, so most apps run both. Deviate when the app needs almost no shared state, when legal transitions are the hard part, when state belongs in the URL, or when an existing store already works (see when).

  • tiny app / a few shared values → useState + Context (no library)
  • complex multi-step flows / statecharts → XState
  • already on Redux and it's fine → keep RTK (migrate only for a concrete pain)
  • already on MobX or Valtio and it works → keep it (store-to-store migrations rarely repay their cost; MobX 7 needs React 18+, see migrate)
  • want fine-grained atoms → Jotai (v3; see migrate for the v2-to-v3 move)
  • leaf-level re-render hot paths where ownership must be decoupled from subscription → Legend State (see reading 'How to Build the Fastest Apps: Break the Rules'; the principle also works with Jotai atoms or Zustand selectors)
  • state that must survive a refresh or a shared link (filters, tabs, pagination, search) → nuqs (in the URL, web; a third bucket, not a Zustand/Query replacement)

Options & tradeoffs

the field considered — and why each one isn’t the default here

optiontradeoffevidence
useState/useReducer + ContextZero-dep; right for local state and low-frequency globals. Every Context consumer re-renders on any value change, so re-render scope is hard to control at scale.
ZustandTiny selector-based store with minimal boilerplate; selectors keep re-renders to the slice read. A State-of-React-2025 favorite (see note).71.5M/wk · ships in 4/34
Jotai (3.x)Atomic, fine-grained subscriptions for naturally derived state. v3 (2026-09-08, verified vs npm) keeps the hooks API but is ESM-only, React 18+, Node ≥22.12, with atomFamily and the babel plugins split out (see migrate).8.0M/wk · ships in 5/34
Redux Toolkit (2.x)Structured, devtools, larger surface; earns its weight on large teams with auditable transitions. React-Redux `connect` HOC deprecated → use hooks.38.3M/wk · ships in 8/34
XState (5.x) / XState StoreState machines / statecharts for flows where legal transitions are the hard part; Store 4.x is a lighter signal-style store.ships in 1/34
TanStack Store / SignalsFine-grained reactive store (alien-signals core) underpinning TanStack Router/Table/Form; usually consumed transitively through those, not picked as the app store.
Legend StateFine-grained observables + built-in sync/persistence; decouples ownership from subscription so re-renders push to leaf nodes; smaller ecosystem than Zustand/Jotai. npm latest 2.1.15 (2024-08-30), v3 on the beta tag (3.0.0-beta.48, 2026-07-12; verified vs npm 2026-09-24).
nuqsType-safe view state in the URL query string: like useState, but shareable, bookmarkable and back-button-safe. A third bucket beside client and server state, not a store replacement. 2.10.1 (2026-08-25, verified vs npm); ~10.8k★. See note: URL STATE6.2M/wk · ships in 4/34
TanStack QueryFor SERVER state/caching; pairs with a client-state lib, doesn't replace it (RB-E-DATA owns it).

evidence: npm weekly downloads (signals snapshot) · “ships in n/D” = adoption across the production-app census, honest denominators

Migration lines

version and deprecation lines this decision tracks — for YOUR repo's sequenced plan: the doctor or npx -y @heart-it/react-brain migrate .

jotai < 3.0.1 → Jotai 3 (≥3.0.1) [upgrade · effort S]

Jotai 3 (2026-09-08) is a MODERNIZATION, not a rewrite — the maintainer's own test is: if your app runs on v2 with no deprecation warnings, it should run on v3 as is. What actually moves: (1) requirements rise to React 18, TypeScript 5.5 and Node >=22.12; (2) the package is ESM-ONLY (no CJS/UMD/SystemJS builds) and `exports` is closed to the public entry points, so any deep import into the package breaks; (3) `atomFamily` moved to the `jotai-family` package and the `jotai/babel` plugins to `jotai-babel` — both are one-line import swaps plus an install; (4) `loadable` is gone (rebuild it over `unwrap`, the guide gives the ~15-line version), as are the read function's `setSelf` option (use onMount or jotai-effect) and the `delay` option on useAtom/useAtomValue (write a custom hook). Two smaller things to know: published files read process.env.NODE_ENV directly, so a bundler-less browser setup must define it, and the build targets ES2020. There is ONE behavior change, not just packaging — a subtle useAtomValue mount-timing shift documented in the guide; useAtomValue now uses React.use() where available (React 19) and falls back to the old path on React 18. 3.0.0 shipped a REGRESSION in that same useAtomValue path (synchronous onMount updates missed by React subscribers), fixed in 3.0.1 (2026-09-29) — land on 3.0.1+, not bare 3.0.0.

github.com/pmndrs/jotai/releases/tag/v3.0.0 · github.com/pmndrs/jotai/releases/tag/v3.0.1 · raw.githubusercontent.com/pmndrs/jotai/main/docs/guides/migr… · registry.npmjs.org/jotai/latest

mobx < 7.0.0 → MobX 7 [upgrade · effort M]

MobX 7 (2026-07-30) drops the ES5/non-proxy fallback entirely — Proxy-backed observables are now the only mode, so `configure({ useProxies })` and `{ proxy: false }` are gone; legacy decorators are no longer supported (Stage-3 `@observer` stays in mobx-react); namespaced properties became named exports (observable.ref → observableRef, comparer.structural → compareStructural); the public `trace` API and mobx-react's Provider/inject were removed, and the React bindings now require React 18+. Pays for itself in bundle: the ESM production build drops 17.02 → 13.96 KiB gzip

github.com/mobxjs/mobx/releases/tag/mobx%407.… · registry.npmjs.org/mobx/latest

npm weekly downloads (from the corpus's last signals run): nuqs 6.2M · zustand 71.5M · jotai 8.0M · @reduxjs/toolkit 38.3M · react-redux 46.7M · redux 54.5M · xstate 6.0M · valtio 2.5M · mobx 4.7M

Verified notes

Server state (cached, can go stale, needs refetch/dedupe/invalidation) and client state (born and dies on the client) are different problems with different tools, so the default is a pair, not one library. State of React 2025 (3,760 devs): TanStack Query is the #1 React library; Zustand crossed 50% usage and tops satisfaction; ~34% use no state library. Local-first (TanStack DB) builds on TanStack Query rather than replacing it. URL STATE (2026-09-10, verified vs nuqs changelog and adapter docs, GitHub and each app's package.json): the entry's 'Component Communication Patterns' reading names URL params for view state as a decision axis; nuqs is the library for it. Adapters are all web (Next.js, React SPA, Remix, React Router, TanStack Router); 2.10 (2026-08-20) lets the React SPA adapter run server-rendered in Astro, Fastify or Hono via a serverSearch prop. In production in four census-cohort apps: Sentry 2.7.1, Cal.com 2.8.2, Supabase 2.7.1, Documenso ^2.8.9. That is a standing adoption level, not a week's movement: census saw them only once this entry gained the nuqs detect row. JOTAI 3 (2026-09-08, verified vs the v3 release and migration guide): a modernization, not a rewrite; an app on v2 with no deprecation warnings should run on v3 as is. The migrate rule owns the checklist. RB-E-DATA owns server state and local-first sync; RB-E-FORMS owns why form fields stay out of the global store; react-native-best-practices owns selector and re-render wiring rules. HISTORY: 2026-10-08 GAP CLOSED: first STATE source on how React Compiler changes store choice — the jjenzz context-vs-store benchmark reading. 2026-10-01 CORRECTED: Jotai 3.0.0 shipped a useAtomValue regression (missed synchronous onMount updates), fixed in 3.0.1 — the migrate rule's floor moved from 3.0.0 to 3.0.1. 2026-09-10 GAP CLOSED: nuqs named for the URL view-state axis (reconciled from harvest/2026-09-03; the manual pass had judged it against RB-E-NAV instead of this entry); Jotai 3 migrate rule added. 2026-06-25 CHALLENGED → SURVIVES (high): nothing has overtaken Zustand; local-first (TanStack DB) builds on TanStack Query rather than replacing it.

Canonical reading

Editorial annotations on why each piece matters — the articles themselves are the originals; read them there.

Making React Context Cheap with React Compilerjjenzz

Measures whether React Compiler removes the usual reason to leave Context for a store or a context-selector library. Every consumer still re-renders on each change, but the compiler reuses JSX and values whose inputs did not change, the same idea as an outer consumer plus a memoized child, applied inside one component. 5,000 radios, p95 in a production build (M3 Pro, Chrome): click 18.4 ms with the compiler, 18.9 ms with a hand-memoized view, 19.2 ms with a useSyncExternalStore store; mount 141.0 / 145.0 / 147.6 ms. The author calls the benchmark narrow (Codex-generated, one scenario) and quotes the React team's position that the compiler already gives most of what people want from context selectors.

Structural sharing, selectAtom, and why your Jotai atoms re-render too muchPeter Piekarczyk

The first Jotai-specific deep-dive here, for the entry's 'fine-grained atoms → Jotai' clause: Object.is propagation mechanics, why selectAtom is a trap (decompose into primitive atoms instead), and write-layer structural sharing à la React Query. The re-render discipline that makes atomic state actually fine-grained.

How to Build the Fastest Apps: Break the Rules (App.js Conf 2026)Jay Meistrich (Software Mansion) — author of Legend State / Legend List

The performance case for signal/observable state. React's `render` over-orchestrates: `useState`/`useContext` couple state *ownership* with *subscription*, so lifting state up and over-subscribed context cascade re-renders top-down — measured ~10x CPU vs updating one leaf node. The fix ('render once'): create state without subscribing (observables / Reanimated shared values), own it high in the tree, push subscription down so only leaf nodes re-render themselves; provide stable objects through Context so the provider never re-renders; subscribe to derived values, not raw ids. Vendor talk (his own Legend State), but the ownership-vs-subscription principle is framework-agnostic and transfers to Jotai atoms, TanStack Store/Signals, and use-context-selector.

React Query as a State ManagerDominik Dorfmeister (TkDodo), TanStack Query maintainer

The canonical case for treating server state as its own thing (stale-while-revalidate, staleTime, query-key sharing) so you stop replicating it in a client store. The source of the server-state-vs-client-state split.

Why React Context is Not a State Management ToolMark Erikson, Redux maintainer

Draws the precise line between dependency injection (Context), client-state stores (Redux/Zustand), and async/server state — the vocabulary that justifies splitting the problem instead of forcing one tool to do all three.

How is Linear so fast? A technical breakdownDennis Brotzky (performance.dev)

Breakdown of Linear's local-first architecture — IndexedDB as the primary store, optimistic updates, granular observables, aggressive code-splitting, GPU-only animation. The reference for building instant-feeling client-state apps.

Component Communication Patterns in React ApplicationsNeciu Dan

A decision framework for WHERE state should live, keyed by component proximity × data type: props/callbacks for adjacent components, lifted state or composition for siblings, context for slow-changing globals, Zustand for frequently-updated client state, TanStack Query for server data, URL params for view state, events for fire-and-forget. 'Reach for the closest tool that can actually reach the problem, and only move outward when it stops working.' The how-to-choose guide that operationalizes this entry's server-vs-client-vs-view split.

Do we need state management libraries anymore?Neci Dan (neciudan.dev)

The build-vs-buy answer behind this entry's '~34% use no state library' and 'don't add a lib' clauses. Reads Redux, Zustand, Jotai, MobX and Valtio under the hood and shows they all converge on the SAME React primitive — useSyncExternalStore — then builds a working store from scratch with selectors, showing which three problems the primitive solves for free and where the naive version loops infinitely. The point is not that libraries are useless but that you should know what ~50 lines of understood code buys you before adding a dependency: what you pay for is ergonomics, devtools and middleware, not the subscription mechanism.

The Absolute State of ManagementAlex Russell

The vocabulary attack on this entry's own subject: Russell argues that Redux, MobX, Zustand — and React itself — don't MANAGE state, they PROPAGATE it, and that real state management is about time and conflicts (the problem CRDTs and sync engines actually solve). Deliberately provocative, and Redux maintainer Mark Erikson pushed back publicly, so read it as a framing argument rather than a verdict. Its durable value here is a third axis next to the client-vs-server split: when your problem is concurrent edits and reconciliation, no propagation library is the answer — that's the local-first/sync-engine row (TanStack DB, Zero — see RB-E-DATA).

Sources

Depth (in-domain rules) is owned by the react-native-best-practices skill — this entry is selection breadth.

The full explanation

The reviewed long-form essay behind this entry — the why, not a how-to. Also on GitHub.

About state management in React & React Native

Diataxis: Explanation. This page builds understanding of the state-management decision — the reasoning behind the pick. It is not a tutorial and not step-by-step how-to: the candidate list and one-line tradeoffs live in the index entry RB-E-STATE; re-render/performance rules live in the react-native-best-practices skill. Read this to understand why; read those to do.

The one distinction that organises everything

Most "which state library?" confusion dissolves once you separate two different problems that the word "state" hides:

  • Server state — data that lives on a server and is cached on the client: it can go stale, needs refetching, deduping, retries, and invalidation. You don't own it; you mirror it.
  • Client (UI/app) state — data that is born and dies on the client: the open tab, a form draft, a theme toggle, a wizard step. You own it outright; it is never "stale."

These have opposite needs, so the leading tools specialise. TanStack Query is a server-state cache, not a client-state store — and Zustand / Jotai / Redux are client-state stores that know nothing about networks. They are complementary, not alternatives. The most common mistake — and the most common source of "Redux is bloated" or "Context is slow" complaints — is using one tool for both jobs: hand-rolling caching/refetch logic inside a client store, or stuffing transient UI flags into a global cache.

So the first question is never "Redux or Zustand?" It is "which kind of state is this?"

The default, and why

For a typical product app, the low-regret 2026 default is:

TanStack Query for server state + Zustand for client state.

This is not fashion. The State of React 2025 survey (3,760 developers) shows TanStack Query as the single most-used React library, and Zustand as the leading dedicated state manager — it crossed 50% usage (up from 28% in 2023), overtook Redux in downloads, and tops satisfaction in its category. When the most-used and most-liked tools also map cleanly onto the server/client split, recommending them is the boring-but-correct choice.

Two consequences worth internalising:

  • You often need less than you think. ~34% of survey respondents use no dedicated state library — useState/useReducer + a little useContext is genuinely enough for many apps. Reach for a library when you feel a real pain (prop-drilling, cross-tree sharing, re-render storms), not pre-emptively.
  • The pieces compose. Adding TanStack Query does not mean removing Zustand, and vice versa. A healthy app frequently runs both, each doing one job.

The landscape, and when each one wins

Treat these as reasons to deviate from the default, not a menu to agonise over.

useState/useReducer + Context — the zero-dependency baseline. Correct for local state and for genuinely low-frequency global values (theme, locale, the authenticated user read once at mount). Its weakness is also its nature: a Context value change re-renders every consumer, so it is the wrong tool for frequently-changing shared state. The failure mode is using one big Context as an app store and then fighting the re-renders — at which point you wanted a store.

Zustand — a tiny selector-based store. It wins when you want shared client state without ceremony: define a store, subscribe to slices with selectors, done. Its selector model is what keeps re-renders surgical, which is exactly where Context falls down. This is the default for most teams that have outgrown plain Context.

Jotai — atomic state, composed bottom-up from primitive "atoms." It wins when state is naturally fine-grained and derived (many small independent pieces, computed values) rather than a few coarse stores. The risk is over-atomisation: splitting state that actually belongs together adds wiring for no benefit.

Redux Toolkit (RTK) — structured, middleware-rich, with the best time-travel devtools. It wins on large teams and complex, auditable state transitions where the explicit, ceremonious flow is a feature (predictability, onboarding, debuggability). RTK fixed most of "classic Redux is boilerplate," and React-Redux now steers you to hooks — the connect HOC is deprecated. The honest guidance: if you are already on a healthy RTK app, stay; migrating to Zustand for its own sake is churn. Choose RTK new mainly when its structure earns its weight.

XState / XState Store — state machines and statecharts. This is a different axis from the others: reach for it when the hard part isn't where state lives but which transitions are legal — multi-step flows, wizards, media players, anything with modes and guarded transitions where "impossible states" are the bug class you keep hitting. XState Store (4.x) is a lighter, signal-flavoured option when you want the modelling without the full interpreter.

TanStack Store / signals — fine-grained reactive primitives (an alien-signals core) that underpin TanStack Router/Table/Form. Most apps consume these transitively via those libraries rather than adopting them directly as the app store; pick them deliberately only when you want signal-style reactivity as a foundation.

URL state — nuqs — a third bucket beside server and client state: view state that must survive a refresh or a pasted link (filters, tabs, pagination, search) belongs in the URL query string, not a store. nuqs gives it a typed useState-like API on web routers (Next.js, React Router, Remix, TanStack Router, plain React SPA). It complements Zustand and TanStack Query rather than replacing either.

Jotai 3 (2026-09-08) keeps the hooks API; the move from v2 is packaging (ESM-only, React 18+, Node ≥22.12, atomFamily in jotai-family), owned by the entry's migrate rule.

The second distinction: ownership vs subscription (the "render once" lens)

The server-vs-client split tells you which tool. A second, subtler distinction explains why some state architectures are fast and others crawl, and it cuts across every library above. useState quietly does two jobs: it creates state and subscribes the creating component to it — ownership and subscription welded together, so the owner must re-render on every change, then pass the value down to whoever uses it. useContext is blunter: it subscribes to the whole context value, not the field you read, so any change anywhere re-renders every consumer. (That is the real mechanism behind "Context is slow" above: a font-scale provider that also carries window size re-renders every text node in the app on a window resize.)

That welding is what makes docs-blessed patterns expensive. Lift state up — "one of the most common things you'll do," per the React docs — moves ownership to the top, so one leaf-level change (a single message's reply colour) cascades a render through the whole screen on its way down. Coordinating through an effect (set state → re-render → open a modal, repause a query, mark-as-read on focus) re-renders when nothing visual changed. A callback with a dependency is recreated when that dependency changes, silently re-rendering its host. None of this is a bug: render is the orchestrator of everything, not just painting. And it is not free — benchmarked, moving an update from the top of the app to the tiniest leaf cut CPU by roughly 10× (Meistrich, App.js 2026). The React Compiler removes hand-memoisation but cannot save you here: when a prop actually changes, it still flows top-down.

The fix is to stop coupling ownership to subscription. Keep ownership high where it's convenient, but hand children stable state objects — references whose identity never changes — and let each consumer subscribe to only the slice it reads and re-render itself. The coordinating screen renders once and then never again; only the leaf that changed does work. These stable objects go by signals, observables, or (in Reanimated) shared values; the label matters less than the property — creating the state doesn't subscribe you; reading it does. That is "render once": render the app and its big coordinating screens once, and let leaf nodes, effects, and callbacks update themselves.

Schematically, using the chat-reply example above:

// BEFORE — ownership + subscription fused at the top; one change re-renders the whole screen
function ChatScreen({ messages }) {
  const [replyId, setReplyId] = useState(null);          // owned here…
  return (
    <>
      {messages.map(m => (
        <Message key={m.id} m={m}
          isReply={m.id === replyId}                      // …and threaded down to every row
          onReply={() => setReplyId(m.id)} />             // setReplyId re-renders ChatScreen → ALL rows
      ))}
      <Composer replyId={replyId} />
    </>
  );
}
// AFTER — ownership stays high, subscription pushed to the leaf; only the affected row re-renders
const reply$ = observable(null);                         // stable object; its identity never changes

function ChatScreen({ messages }) {                      // renders once — it never *reads* reply$
  return (
    <>
      {messages.map(m => <Message key={m.id} m={m} />)}
      <Composer />
    </>
  );
}

function Message({ m }) {
  const isReply = use$(() => reply$.get() === m.id);     // subscribe to a *derived* slice
  return <Row highlighted={isReply} onReply={() => reply$.set(m.id)} />;  // set from anywhere, no prop-drill
}

ChatScreen never reads reply$, so it renders once; each Message subscribes to only the === m.id slice, so setting reply$ re-renders exactly the one row whose value flipped — not the list, not the screen. The observable/use$/.get()/.set() shape is Legend State's; a Jotai atom, a Zustand selector, or use-context-selector expresses the same shift. The point isn't the API — it's that creation and subscription are now separate lines.

This is neither exotic nor one vendor's trick. You already rely on it whenever a Reanimated shared value drives an animation off the render path; it is why Legend List stays smooth (a fixed pool of containers that signal themselves to re-render, instead of re-rendering the list — see RB-E-LISTS); and it is the default model of Solid, Svelte, and Preact. Within this entry's own landscape, Jotai already leans this way (atoms decouple a value from its reader) and selector stores like Zustand narrow subscriptions to slices; dedicated signal/observable libraries (Legend State, TanStack Store, and use-context-selector for Context specifically) take it furthest.

Two honest caveats. It departs from the React-docs defaults (lift-state-up, effects for coordination), so adopting it wholesale is an architectural choice a team buys into, not a drop-in — and its sharpest articulation comes from the author of Legend State, so weigh it as a lens, not a mandate; the ownership-vs-subscription reasoning survives dropping any particular library. And it stays Explanation-level here: the mechanics — selector placement, avoiding context-driven list re-renders, atomic vs coarse subscriptions — live in react-native-best-practices, and the candidate libraries in the index entry. Hold just the model: decide who owns state separately from who subscribes to it, and push the subscription as far down the tree as it goes.

Tradeoffs and failure modes to name out loud

  • "Context is slow" is usually "I used Context as a high-frequency store." The fix is a selector-based store (Zustand/Jotai), not abandoning Context for the things it's good at.
  • "Redux is boilerplate" largely predates RTK. Judge RTK, not 2018 Redux. But also don't add RTK to a small app to look serious — that's the inverse mistake.
  • Over-atomisation (Jotai) / over-storification — splitting cohesive state into many atoms or stores trades one problem (too coarse) for another (too scattered). Group state that changes together.
  • Server state in a client store — the cardinal error. If you find yourself writing isLoading, error, refetch and cache-invalidation logic by hand inside Zustand/Redux, that state wanted TanStack Query (see RB-E-DATA).

How it interacts with the rest of the stack

  • React Compiler (RB-E-REACT-CORE). With the compiler auto-memoising, the old performance argument "a store avoids re-renders you'd otherwise hand-memoise" weakens for client state — choose a store for its ergonomics and sharing model, not as a memoisation workaround. One caveat the ecosystem has flagged: mutating objects you got from useState can defeat compiler optimisation — prefer immutable updates and data-first models over behaviour-rich class instances in render paths.
  • React Native re-render performance. The depth rules — selector discipline, avoiding context-driven list re-renders, atomic vs coarse subscriptions — are owned by react-native-best-practices (e.g. its atomic-state rule). This page tells you which tool; that skill tells you how to wire it without jank.
  • Forms (RB-E-FORMS) are their own kind of state. Don't model form fields in your global store; a form library (React Hook Form, TanStack Form) owns that lifecycle.
  • Cross-platform (RB-E-CROSSPLATFORM). All the client-state options here are platform-agnostic and belong in your shared logic package, working identically on web and React Native — a good thing to centralise once.

Migrating (the short version)

Migrations between client stores are usually not worth it without a concrete pain — the libraries are similar enough that the win rarely covers the cost. The migration that does pay off is the conceptual one: pulling server state out of a client store into TanStack Query. That typically deletes code (hand-rolled caching, loading flags, refetch effects) rather than moving it, which is the signal of a good migration. Keep detailed step-by-step migration mechanics out of this page — that is a how-to concern; the principle is "move by responsibility, server-state first."

In one paragraph

Decide kind of state before library. Default to TanStack Query (server) + Zustand (client); stay on healthy Redux Toolkit; reach for Jotai when state is naturally atomic, XState when legal transitions are the hard part, and plain useState/Context when you need nothing more — which is more often than the discourse suggests. The recurring mistake is using one tool for both kinds of state. And past which tool lies how you wire it: decouple who owns state from who subscribes to it, pushing re-renders down to the leaves ("render once").


See also: RB-E-DATA (server-state / data-fetching detail), RB-E-FORMS, RB-E-REACT-CORE (Compiler interaction), RB-E-CROSSPLATFORM. Depth on re-render performance: the react-native-best-practices skill.

Related in app-architecture: data · p2p · nav · meta-frameworks · forms · auth · networking · uploads · crossplatform · desktop