When React first appeared on the scene, one of its most compelling features was its “one-way data flow”. This is still outlined in the React docs under the page “Thinking in React”
The component at the top of the hierarchy will take your data model as a prop. If you make a change to your underlying data model and call
root.render()again, the UI will be updated. You can see how your UI is updated and where to make changes. React’s one-way data flow (also called one-way binding) keeps everything modular and fast.
The idea is that data can only flow one-way through your app which therefore makes your app much easier to intuit, understand, and reason about.
This came to be summarized in the phrase: “UI is a function of state”, or
ui = fn(state). Whenever some state changes, due to an action, the view re-renders. To date, a number of sophisticated “state management” solutions have been created to facilitate building applications with this mental model.
The rarely-acknowledged problem here, however, is that this “one-way data flow” is a bit of a misnomer. It’s really a one-way data flow on the client. But having data exclusively on the client is rarely practical. Most of the time you need to persist data—to sync it—which means you need data to flow two ways: between the client and the server.
A lot of state management tools only help you manage state on the client but they don’t help you effectively cross the network chasm: the gap between the state on the client and the state on the server.
Enter Remix: “one of the primary features of Remix is simplifying interactions with the server to get data into components.” Remix extends the flow of data across the network, making it truly one-way and cyclical: from the server (state), to the client (view), and back to the server (action).
When it’s said that your “UI is a function of state”, a more nuanced way of disentangling the assumptions in that statement would be: UI is a function of your remote state and your local state. In a traditional React app, all state lives on the client and the pieces you want to persist must jump outside “the one-way data flow” and be synced across the network to a server. As you can imagine, this is an area prone to bugs.
In Remix, however, the idea of "UI as a function of state" is transformed because remote state can be more easily disentangled from local state. “What’s the difference,” you ask? Think of it this way.
Remote state is any data that needs to persist, like user data. This state (e.g. how many unread notifications does the user have?) is stored off the client and reconciles into your app from Remix mechanisms like loaders and actions.
(Note: Remix helps you cross the network chasm by also providing stateful information around the transmission of your persistent data via transitions and fetchers — no need to track the status of every network request yourself via booleans like
isLoading or enums like
initial | loading | success | failed).
In contrast, local state is ephemeral data which can be lost (e.g. via a refresh) without negatively impacting the user experience. This state (e.g. is the dropdown open which reveals the user’s notifications?) is stored on the client via mechanisms like React state or local storage. Importantly, it does not need to persist and sync across the network thereby reducing complexity and the potential for bugs.
Forms, fetchers, loaders, actions, these are all “state management” solutions in Remix (though we don’t call them that). They give you the tools to keep persistent state in sync between the client and the server, ensuring data flows cyclically one-way through your app and across the network: from loaders to a component to an action and back again.
With Remix, your UI becomes a function of state across the network, not just locally. An interesting analogy to Remix’s data abstractions is React’s virtual DOM abstraction.
In React, you don’t worry about updating the DOM yourself. You set state and the virtual DOM does all the diffing to figure out how to make efficient updates to the DOM. Remix extends this idea to the API layer for persistent data.
In Remix, you don’t worry about keeping client-side state in sync with the server. You “set state” with a mutation and the loaders take over to refetch the most up-to-date data and make updates to your component views.