Mobile Development

State Management: Unidirectional Data Flow

Hacker Way, 2014

Facebook engineer Jing Chen presented Flux at F8 in May 2014. A year later, Dan Abramov released Redux at React Europe 2015, simplifying Flux to a single Store and pure reducers. The idea proved so precise that Apple and Google independently arrived at the same architecture - SwiftUI Combine and Android StateFlow reproduce the unidirectional flow without a single reference to Flux.

The unread messages badge shows 1, but the chat is empty. Open it again - still 1. This bug in Facebook Messenger survived for months with no fix. Not because engineers did not know Objective-C. But because MVC was never designed for 50 controllers sharing one piece of state. The solution they built changed how UI is written for the next 10 years.

  • Instagram (Meta): Redux-like state for feeds, Stories, Direct - billions of events per second
  • Discord: moved from MVC to Flux in 2016, eliminating a class of race conditions in voice channels
  • Shopify Mobile: ViewModel + StateFlow on Android, ObservableObject on iOS - one pattern, two languages
  • Airbnb: switched from Redux to MobX - same unidirectional principle, less boilerplate

The MVC Problem: How Facebook Broke

2014. Facebook Messenger shows 1 unread message. Open the chat - nothing there. Go back - still 1. The bug survives for months, appearing and vanishing, impossible to reproduce consistently. Facebook engineers search for the cause and find not a bug - but an architectural catastrophe.

In classic MVC, controllers talk to models, models notify views, views sometimes touch models directly, models call other models. The dependency graph becomes spaghetti. When two different controllers - NotificationsController and ChatController - update the same UnreadCount model in an unpredictable order, state becomes non-deterministic. The race condition is not in threads. It is in the data flow.

Facebook engineers described the problem publicly at F8 2014 - this became known as the "Hacker Way" manifesto. Instead of hunting for a bug in the code, they changed the architecture. The principle turned out to be simple: data must flow in one direction, and only one direction.

The same principle later became the foundation of Redux (Dan Abramov, 2015), MobX (Michel Weststrate, 2015), SwiftUI Combine, and Android StateFlow. One chat bug at Facebook changed how UI is written for the next decade.

Why was the Facebook unread messages bug difficult to reproduce?

Flux/Redux: One Flow, One Truth

Flux is not a library. It is a pattern with four roles: Action, Dispatcher, Store, View. Data flows strictly in a circle, and never in the opposite direction. The View does not touch the Store directly. The Store does not know about the View. Between them sits an Action - the intent to change state.

Redux added one idea to Flux: the entire application state is one object. One Store. A reducer is a pure function `(state, action) => newState`. No side effects inside, no mutation. This made application state deterministic: the same sequence of actions always produces the same state. Time-travel debugging became possible.

React Native with Redux has been the de facto standard for large applications. Instagram, Discord, and Shopify use Redux or its derivatives. Predictability is worth more than development speed. Production bugs cost more than boilerplate.

MobX is an alternative with a different approach: instead of immutability and reducers - reactive observable objects. Mutation is allowed, but only through action decorators. Less boilerplate, more implicit behavior. Popular in teams with Angular backgrounds.

What does a reducer do in Redux?

SwiftUI, Compose, StateFlow: One Pattern, Three Platforms

When Apple released SwiftUI in 2019 and Google released stable Jetpack Compose in 2021, both frameworks arrived with one built-in idea: UI as a function of state. Not imperative 'update label.text', but declarative 'redraw the screen when state changes'.

React Native in 2024 also evolved: useState/useReducer for local state, Zustand or Jotai instead of heavyweight Redux for global state. The principle remains the same - state owns the data, UI reflects it. The direction of flow - only top-down.

Key platform difference: SwiftUI @State lives in View memory and resets when the View is recreated. @StateObject/@ObservedObject survives re-renders. Android ViewModel survives configuration changes (screen rotation) - that is precisely why it exists.

@State in SwiftUI is the same as @ObservedObject

@State is local state owned by one View, created and destroyed with it. @ObservedObject is an external class object that survives re-renders and can be shared between Views

The confusion is expensive: using @State for a ViewModel means data resets on every View recreation. Rule: @State for simple primitives (Bool, Int, String), @StateObject/@ObservedObject for ViewModels and complex objects

Why is MutableStateFlow private but StateFlow public in Android ViewModel?

Key Ideas

  • **Unidirectional flow** - data flows Action -> State -> UI, never in reverse
  • **Single Source of Truth** - one place for state storage, not dozens of scattered variables
  • **Immutability** - state is not mutated, a new object is created. This makes state reproducible
  • **@State vs @ObservedObject** - local vs shared state. Mixing them resets data on re-renders
  • **StateFlow on Android** - MutableStateFlow private (only ViewModel writes), StateFlow public (View only reads)

Related Topics

State management builds on architectural patterns and leads to more advanced UI architecture topics:

  • State Management: MVI, MVVM on Android — MVI and MVVM are concrete implementations of unidirectional flow for Android
  • UIKit and iOS Architectures — MVC in UIKit - what SwiftUI with @State/@ObservedObject moves away from
  • Design Patterns: GoF — Observer, Command, Memento - patterns underlying Flux/Redux

Вопросы для размышления

  • Facebook solved bidirectional MVC chaos with unidirectional Flux. Another approach is component isolation: each component owns its state, no shared models. When is global state necessary, and when is local state sufficient?
  • Redux requires an action, reducer, and selector for every change - heavy boilerplate. MobX allows mutating objects through actions, less code but more implicit behavior. What matters more in a production app: predictability or development velocity?
  • SwiftUI @StateObject survives re-renders but not View destruction. Android ViewModel survives rotation. What happens to state on a force-kill? Where is the boundary between in-memory state and persistent state?

Связанные уроки

  • mob-11 — MVI/MVVM on Android builds on these same principles
  • mob-04 — UIKit and iOS architectures - prerequisite context
  • prog-13-patterns — Observer pattern is the direct ancestor of reactive streams
  • se-05 — GoF patterns in the context of UI architectures
  • ds-02-cap-theorem — CAP trade-offs mirror state trade-offs: consistency vs availability
  • comp-01-intro
State Management: Unidirectional Data Flow

0

1

Sign In