惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Schneier on Security
Schneier on Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Attack and Defense Labs
Attack and Defense Labs
H
Hacker News: Front Page
Google DeepMind News
Google DeepMind News
雷峰网
雷峰网
C
CXSECURITY Database RSS Feed - CXSecurity.com
Cisco Talos Blog
Cisco Talos Blog
T
Tenable Blog
G
Google Developers Blog
A
About on SuperTechFans
The Cloudflare Blog
S
Securelist
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
C
Cisco Blogs
H
Hackread – Cybersecurity News, Data Breaches, AI and More
aimingoo的专栏
aimingoo的专栏
云风的 BLOG
云风的 BLOG
Forbes - Security
Forbes - Security
腾讯CDC
Application and Cybersecurity Blog
Application and Cybersecurity Blog
V
Vulnerabilities – Threatpost
IT之家
IT之家
博客园_首页
P
Proofpoint News Feed
P
Privacy & Cybersecurity Law Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
Project Zero
Project Zero
月光博客
月光博客
NISL@THU
NISL@THU
爱范儿
爱范儿
S
Secure Thoughts
K
Kaspersky official blog
Security Latest
Security Latest
T
Tailwind CSS Blog
博客园 - Franky
D
Darknet – Hacking Tools, Hacker News & Cyber Security
TaoSecurity Blog
TaoSecurity Blog
The GitHub Blog
The GitHub Blog
Microsoft Azure Blog
Microsoft Azure Blog
B
Blog RSS Feed
S
SegmentFault 最新的问题
H
Help Net Security
T
Tor Project blog
L
LINUX DO - 热门话题
S
Security @ Cisco Blogs
N
News and Events Feed by Topic
O
OpenAI News
S
Schneier on Security

Dropbox Tech Blog

How we used DSPy to turn AI evaluations into better responses in Dash chat How Dropbox uses MCP and Dash to close the design-to-code security gap Beyond code generation: rethinking engineering productivity in the age of AI agents Introducing Nova, our internal platform for coding agents Improving storage efficiency in Magic Pocket, our immutable blob store Reducing our monorepo size to improve developer velocity How we optimized Dash's relevance judge with DSPy Using LLMs to amplify human labeling and improve Dash search relevance How low-bit inference enables efficient AI Insights from our executive roundtable on AI and engineering productivity Engineering VP Josh Clemm on how we use knowledge graphs, MCP, and DSPy in Dash Inside the feature store powering real-time AI in Dropbox Dash Building the future: highlights from Dropbox’s 2025 summer intern class Fighting the forces of clock skew when syncing password payloads Introducing Focus, a new open source Gradle plugin Making camera uploads for Android faster and more reliable How Dropbox Replay keeps everyone in sync Why we built a custom Rust library for Capture Detecting memory leaks in Android applications How we sped up Dropbox Android app startup by 30% Why we chose Apache Superset as our data exploration platform Revamping the Android testing pipeline at Dropbox Our counterintuitive fix for Android path normalization JQuery to React: How we rewrote the HelloSign Editor How we ensure credible analytics on Dropbox mobile apps Engineering Dropbox Transfer: Making simple even simpler Speeding up a Git monorepo at Dropbox with <200 lines of code Building for reliability at HelloSign Store grand re-opening: loading Android data with coroutines Modernizing our Android build system: Part I, the planning Modernizing our Android build system: Part II, the execution Our journey to type checking 4 million lines of Python The (not so) hidden cost of sharing code between iOS and Android The Programmer Mindset: Main Debug Loop On working with designers Incrementally migrating over one million lines of code from Python 2 to Python 3 Crash reporting in desktop Python applications What we learned at our first JS Guild Summit How we rolled out one of the largest Python 3 migrations ever Dropbox Paper: Emojis and Exformation Creating a culture of accessibility Adding IPv6 connectivity support to the Dropbox desktop client Accelerating Iteration Velocity on Dropbox’s Desktop Client, Part 2 Accelerating Iteration Velocity on Dropbox’s Desktop Client, Part 1 DropboxMacUpdate: Making automatic updates on macOS safer and more reliable Annotations on Document Previews Open Sourcing Pytest Tools Open Sourcing Zulip – a Dropbox Hack Week Project Building Carousel, Part III: Drawing Images on Screen The Tech Behind Dropbox’s New User Experience on Mobile (Part 2) Building Dropbox’s New User Experience for Mobile, Part 1 Building Carousel, Part II: Speeding Up the Data Model Building Carousel, Part I: How we made our networked mobile app feel fast and local Scaling MongoDB at Mailbox Welcome Guido! Dropbox dives into CoffeeScript Some love for JavaScript applications Plop: Low-overhead profiling for Python Using the Dropbox API from Haskell A Python Optimization Anecdote Translating Dropbox
Redux with Code-Splitting and Type Checking
Matthew Gerstman · 2019-07-16 · via Dropbox Tech Blog

Before We Get Started

This article assumes a working knowledge of Redux, React, React-Redux, TypeScript, and uses a little bit of Lodash for convenience. If you’re not familiar with those subjects, you might need to do some Googling. You can find the final version of all the code here.

Introduction

Redux has become the go-to state management system for React applications. While plenty of material exists about Redux best practices in Single Page Applications (SPAs), there isn’t a lot of material on putting together a store for a large, monolithic application.

What happens when you only need a few reducers on each page, but it could be any permutation of the total number of reducers you support? How do you code-split your store so you’re not serving unnecessary JavaScript on a single page? And while you’re working on code splitting, how do you get it to play nicely with TypeScript so that you can trust what’s going in and coming out of the store?

The Architecture

Before we dive into code, let’s outline the architecture we’re about to build.

We need to create the store in such a way that we can register reducers asynchronously. This allows us to async load code associated with those reducers.

We need to type the store in such a way that it knows about all possible reducers we can register. This allows us to ensure static typing of all components at runtime.

Creating the Store

In order to code split, we need to instantiate the store in a way that allows us to register reducers after store creation. We start with the following code:


// redux-utils/store.ts

import { combineReducers, createStore, Store } from "redux";
import { ReducerMap, StoreShape } from "./types";

let reducerMap: ReducerMap = {};
const store = createStore(combineReducers(reducerMap));

export function getStore() {
  return store;
}

export function registerReducer(newReducers: ReducerMap): Store {
  reducerMap = { ...reducerMap, ...newReducers };
  // We probably want to validate we're not replacing reducers that already
  // exist here but that exercise is left up to the reader.
  store.replaceReducer(combineReducers(reducerMap));
  return store;
}

What’s going on here? We’re instantiating the store with the file and we’ve also exported two functions. One called getStore, which is simply a wrapper around the store and doesn’t need much further explanation, and registerReducer.

registerReducer is the more interesting function. We maintain a map of existing reducers internal to the module and then replace add new ones as they come in. We then call replaceReducer on the store and replace it wholesale. replaceReducer is smart enough to maintain the state of the reducers that were previously there and fires an INIT action for the new ones to populate their default state.

This is what makes code-splitting possible. We don’t care when the reducer is registered and all of that code can be loaded after the store is created.

Type Safety

Now let’s dig into what makes this type safe. Well, let’s dig into our types.ts file.

// redux-utils/types.ts

import { NonwizardNamespaceShape } from "../data/nonwizard/types";
import { WizardNamespaceShape } from "../data/wizards/types";
import { Reducer } from "redux";
export const NONWIZARD_NAMESPACE_KEY = "NONWIZARD_NAMESPACE";
export const WIZARD_NAMESPACE_KEY = "WIZARD_NAMESPACE";

export type FullStoreShape = {
  [NONWIZARD_NAMESPACE_KEY]: NonwizardNamespaceShape;
  [WIZARD_NAMESPACE_KEY]: WizardNamespaceShape;
};
export type StoreShape = Partial<FullStoreShape>;
export type NamespaceKey = keyof StoreShape;
export type ReducerMap = Partial<
  {[k in NamespaceKey]: Reducer<FullStoreShape[k]>}
>;

You’ll notice we import NonwizardNamespaceShape and WizardNamespaceShapefrom elsewhere in the codebase. This is okay. Because these are type-only imports, most build systems won’t actually bundle them in when building packages. This is where the statically typed code splitting magic happens.

We then export types StoreShape and ReducerMap, which allow us to register all possible types on the actual state object in advance. Because we colocate the namespace keys in the types file, our developers can ensure that there are no key conflicts.

You’ll notice these types are both Partial, so how do we enforce that a reducer is actually registered? Well, we do that in the selector layer.

Selectors

Our selector layer is what ensures that we always have the reducers registered that we need. We can do this with a simple helper function.

	
// redux-utils/selectors.ts

import { FullStoreShape, NamespaceKey, StoreShape } from "./types";
export function getStateAtNamespaceKey(
  state: StoreShape,
  namespace: T
): FullStoreShape[T] {
  const namespaceState = state[namespace];
  if (!namespaceState) {
    throw new Error(
      `Attempted to access state for an unregistered namespace at key ${namespace}`
    );
  }
  // We need to explicitly say this is not undefined because TypeScript doesn't 
  // recognize the thrown error will prevent us from ever getting here.
  return namespaceState!;
}

In short, getStateAtNamespaceKey complains very loudly if you attempt to access a namespace that hasn’t been registered yet. This is the only way we should access our data. As long as you call registerReducer in the same part of your tree as your <Provider /> component, your namespace should be registered by the time you get down to a connect. We’ll elaborate on this in a moment.

Writing Actual Product Code

This is is all well and good, but let’s talk about what our product code looks like.

//wizards.tsx

import * as React from "react";
import { connect, Provider } from "react-redux";
import { getStoreForWizardApp } from "./data/wizards/store";
import { Wizard } from "./data/wizards/types";
import { getWizards } from "./data/wizards/selectors";
export function WizardApp() {
  return (
    <Provider store={getStoreForWizardApp()}>
      <ConnectedWizards />
    </Provider>
  );
}
function Wizard({ name, spells, hasWand }: Wizard) {
  return (
    <>
      <span>Name: {name}</span>
      <span>Parents: {parentsAlive ? "Alive" : "Dead"}</span>
      <span>Learned Spells: {spells.map(spell => `${spell} `)}</span>
    </>
  );
}
type WizardsProps = { wizards: Wizard[] };
function Wizards({ wizards }: WizardsProps) {
  return (
    <>
      {wizards.map(wizardProps => (
        <Wizard {...wizardProps} />
      ))}
    </>
  );
}
const mapStateToProps = state => ({
  wizards: getWizards(state)
});
const ConnectedWizards = connect(mapStateToProps)(
  Wizards
);

The code above is (hopefully) straightforward. We connect to the Redux store using react-redux and use the <Provider /> and connect component/HOC respectively. We take a list of wizards and render them out to the screen, along with information about what spells they know and the status of their parents. Spoiler: We’re getting to a certain boy wizard with a lightning scar.

The two novel bits of code here are getStoreForWizardApp and getWizards. Let’s dig into them both.

getStoreForWizardApp

	
// data/wizards/store.ts

import { getStore, registerReducer } from "../../redux-utils/store";
import { WIZARD_NAMESPACE_KEY } from "../../redux-utils/types";
import { once } from "lodash";
import wizardReducer from "./reducer";
export const getStoreForWizardApp = once(() =&gt; {
  registerReducer({ [WIZARD_NAMESPACE_KEY]: wizardReducer });
  return getStore();
});

In the above code, you saw what we call the registerReducerand getStorefunctions that we declared before. We pass registerReducera map with the key for the Wizard namespace and the Wizard reducer. Another important note: if we try to pass the wrong key or even the wrong reducer to registerReducer, type checking will complain about it.

One last but crucial bit. We wrap getStoreForWizardApp in lodash.once. This ensures that we only register the reducer once and then always return the same instance of the store. While this isn’t strictlyrequired, replaceReduceris an expensive noop, if called repeatedly.

getWizards

	
// data/wizards/selectors.ts

import { getStateAtNamespaceKey } from "../../redux-utils/selectors";
import { StoreShape, WIZARD_NAMESPACE_KEY } from "../../redux-utils/types";
import { mapValues } from "lodash";
import { Wizard } from "./types";

const getWizards = (state: StoreShape) =&gt; (
  getStateAtNamespaceKey(state, WIZARD_NAMESPACE_KEY)
);

This one is much more straightforward. We call getStateAtNamespaceKeyand spit out the wizards to the user.

Actions

Sweet! We’ve set up the store, registered our reducer, and even built some components. Now let’s talk about how we can strongly type our actions. We do this in both the action layer and the reducer layer.

	
// data/wizards/actions.ts

import { AnyAction } from "redux";
export type Wizard = {
  name: string;
  hasWand: boolean;
  spells: string[];
};
export const enum WizardActionTypes {
  LearnSpell = "WIZARD/LEARN_SPELL",
  BreakWand = "WIZARD/BREAK_WAND"
}
export type WizardNamespaceShape = {
  [id: string]: Wizard;
};
export interface LearnSpellAction extends AnyAction {
  type: WizardActionTypes.LearnSpell;
  payload: { id: string; spell: string };
}
export interface BreakWandAction extends AnyAction {
  type: WizardActionTypes.BreakWand;
  payload: { id: string };
}
export type WizardAction = LearnSpellAction | BreakWandAction;

You’ll notice that we have two action types: LearnSpellActionand BreakWandAction. These actions each have a strongly-typed payload and their type is a predetermined string enum. We also export WizardAction, which is useful in our reducer.

	
// data/wizards/reducer.ts

import { WizardAction, WizardActionTypes, WizardNamespaceShape } from "./types";
const defaultState: WizardNamespaceShape = {
  powerfulWizard: {
    name: "Powerful Wizard",
    hasWand: false,
    spells: []
  }
};
export default function reducer(
  state: WizardNamespaceShape,
  action: WizardAction
) {
  switch (action.type) {
    case WizardActionTypes.breakWand: {
      const { id } = action.payload;
      return {
        ...state,
        [id]: {
          ...state[id],
          hasWand: false
        }
      };
    }
    case WizardActionTypes.LearnSpell: {
      const { id, spell } = action.payload;
      return {
        ...state,
        [id]: {
          ...state[id],
          spells: [...state[id].spells, spell]
        }
      };
    }
    default: {
      return state;
    }
  }
}

This is one of those occasions where TypeScript is truly brilliant. Our given action type is any of the WizardActionTypes. Because each of them has their own defined typeproperty, our switch statement will actually strongly type action.payloadafter we determine its type. If we were to put any invalid code here, TypeScript would complain.

Store Hydration

The last question to answer here is: “How do we get initial data into the store?” That’s done through a process called store hydration. What this means is that we’re going to dispatch an action that sets the state. Let’s take a look at this code. First, we update our actions.tsfile as shown.

	
export const enum WizardActionTypes {
  Hydrate = "WIZARD/HYDRATE",
  LearnSpell = "WIZARD/LEARN_SPELL",
  BreakWand = "WIZARD/BREAK_WAND"
}

export interface HydrateWizardsAction extends AnyAction {
  type: WizardActionTypes.Hydrate;
  payload: WizardNamespaceShape;
}
export type WizardAction =
  | HydrateWizardsAction
  | LearnSpellAction
  | BreakWandAction;

Second, we add another switch statement to our reducer.

	
switch (action.type) {
  case WizardActionTypes.Hydrate: {
    return {
      ...action.payload
    };
  }
  case WizardActionTypes.BreakWand: {

Third, we need to make an action creator.

	
// data/wizards/actions.ts

import { WizardActionTypes, WizardNamespaceShape } from "./types";

export function hydrateWizardNamespace(initialData: WizardNamespaceShape) {
  return {
    type: WizardActionTypes.Hydrate,
    payload: initialData
  };
}
// LearnSpell and BreakWand are exercises left up to the reader

Finally, we dispatch the hydration action from our store creation function.

	
import { getStore, registerReducer } from "../../redux-utils/store";
import { WIZARD_NAMESPACE_KEY } from "../../redux-utils/types";
import { once } from "lodash";
import wizardReducer from "./reducer";
import { WizardNamespaceShape } from "./types";
import { hydrateWizardNamespace } from "./actions";
export const getStoreForWizardApp = once(
  (initialData?: WizardNamespaceShape) =&gt; {
    registerReducer({ [WIZARD_NAMESPACE_KEY]: wizardReducer });
    const store = getStore();
    if (initialData) {
      store.dispatch(hydrateWizardNamespace(initialData));
    }
    return store;
  }
);

The lodash.once is now extra useful because we will only ever populate the store once.

Conclusions

I hope this article helped you get started with Redux. At compile time, our store is strongly typed and has knowledge of the entire system. At runtime, we can code split however we’d like.

Glossary of Functions

This is a list of the core functions and types and the roles they serve in this architecture.

Types

  • NamespaceKey — A key for a reducer or namespace within the state object.
  • ReducerMap — Object of all possible keys we can have on our store and their matching reducers. Is declared as a partial because it is not guaranteed that any given namespace is on the store.
  • StoreShape — Object of all possible keys we can have on our store and their state shapes. Is declared as a partial because it is not guaranteed that any given namespace is on the store.

Functions

  • hydrateWizardNamespace — Product layer function that provides initial state for the wizard namespace after the reducer is registered.
  • getStoreForWizardApp — Product layer function that registers the “wizard” namespace within the store.
  • getStateAtNamespaceKey — Function that grabs a namespace from the state object and fails quickly if that namespace is unregistered. Used to make our selectors type safe.
  • registerReducer — Function that injects a reducer into the store after page load. Ensures that we only register known reducers at the typing layer.

This post was originally posted on Matthew Gerstman’s personal blog. Links to original gists as well as a list of acknowledgements can be found there. You can follow Matthew on Twitter @MatthewGerstman.