Young Reacts #92 – Remote pay tradeoff

Silicon Valley companies are having debates about remote work. Not only is there a question of whether to go remote-first or not, but there is also one of whether to adjust pay. Teams have to think through three angles:

  • Should they be paid according to their contributions to the company, which may not change meaningfully based on their locations of work?
  • Should there be an adjustment based on cost-of-living so that employees don’t feel more incentivized to work remotely?
  • Should there be an adjustment based on cost-of-labor so that employers can pay competitively across different labor markets?

I find them all valid perspectives to have, which makes the debates all the more difficult.

And there is an issue of reliable data availability. Industry-specific cost-of-labor data are sparse in non-tech hubs. While changing compensation policies is tricky, the companies should get ready to learn and adjust their systems as their first tries at the problem play out in the talent market.

Photo by Christine Roy on Unsplash


Software Engineering ⚙️

We need more inclusive web performance metrics – Scott Jehl

The typical web performance metrics do not answer vital accessibility questions such as:

  • When and how are accessibility trees built and exposed?
  • Does client-side javascript block executions of assistive technologies?

Avoid Export Default – TypeScript Deep Dive

I’ve instinctively avoided default exports in Typescript. But it is nice to see the drawbacks listed. To me, the most significant pain points are the lack of autocomplete and typo protections.

People ❤️

How I operated as a Staff engineer at Heroku – Amy Unger

If one stays at the same place long enough to operate at the Staff level, it’d be easy to accept the status quo—because they are the status quo. Amy suggests that old-timers should “invest in meeting new folks as a counterbalance.” I agree 100% and took a note of it to follow through.

Every Public Engineering Career Ladder – Shawn Wang

This collection of career ladders would have been useful in my previous role. I thought I’d share for those who influence their teams’ career tracks.

Business 💸

Xbox All Access seems like one of the best deals in gaming – Ars Technica

Xbox All Access is an enticing proposition for which I would seriously consider signing up. Rather than competing with Sony on Sony’s turf, Microsoft leverages its investment in the cloud infrastructure and creates a new axis to compete—subscription and streaming games. A lot of transactional consumer products and experiences now are subscribable.

Young React #85 – What Does Sponsorship Look Like?

My wife has started her Master’s study at UC Berkeley, which is super exciting (She wrote about her journey to grad school here). Since she will take her classes remotely, I will be able to eavesdrop on her classes and get some free education as well. I am especially looking forward to a case-study class of tech companies.

However, we also ran into a predicament when she and I had meetings/classes at the same time. We live in a one-bedroom apartment, and our desks are sitting right next to each other. When we speak at the same time, our voices bleed into the other’s microphone and get in the way. But it’s already too late to get a bigger place since we just signed a new lease. Well, this explains why the housing price in the area went up.

Photo by Petr Macháček on Unsplash


Software Engineering ⚙️

TypeScript React Apollo – The Guild

In my current GraphQL-Typescript setup, I have to manually type individual useQuery and useMutation hooks, which is tedious and error-prone. This code generation tool autogenerates the typed hooks.

Relatedly, graphql-typescript-definitions from Shopify alleviates that problem by providing the necessary interfaces right from the graphql document. But it still requires manual typing at the end.

Svelte ❤ TypeScript – Svelte

The Svelte team recently announced that Svelte now supports Typescript. I have to admit that I had quite a simplistic view of what it means to support Typescript before reading this. The integration was quite involved since Svelte has its syntaxes and language server; the Svelte team extended its language server to understand Typescript and provide the language information to IDE based on the common interface.

jest-axe – Nick Colley

This accessibility testing tool integrates with Jest so that you can unit-test your components for their accessibility. I love that I only have to assert toHaveNoViolations at the container element. A word of caution though: this test does not guarantee your app’s accessibility.

People 💖

What Does Sponsorship Look Like? – Lara Hogan

I learned that, at a larger org, my access to opportunities (visible and impactful projects) matters as much as my ability to execute them. Without the opportunity, I can’t learn from experiences. Without the experience, few will offer the opportunity. Sponsorship, promoting others for the new opportunities, is a great tool to break that logjam. And I am deeply grateful to Lauren for giving me that chance.

Business 💸

Content, Cars, and Comparisons in the “Streaming Wars” – Matthew Ball

I always found how content accounting works strange. All content spends are treated as assets, and they are amortized over the same number of years when some turn out to be a dud right off the gate. The article reminds me that, even if accounting works mechanically, the quality of execution (talents, and IP) matters.

Young React #81 – Being Responsible For Other Lives

My wife and I decided to foster two kittens for the next five weeks. We didn’t make the decision lightly. But as we drove home with the kittens, it dawned on me that this would be the first time I am responsible for other lives.

Since fostering is short, it is not the same as adopting pets, or, much less, having kids. Nonetheless, I feel the pressure and responsibility. I have to feed them regularly, give them medications, and socialize them so that they will find a caring permanent home. To fulfill their needs, I had to adjust some of my routines and tolerate getting disturbed while working.

After a few days, I realize I never fully understood what working parents struggle with: new priorities, daily distractions, and energy drain. I knew it with my head but never felt it myself. This is another reminder that genuine empathy is hard without actually experiencing something similar.

Meet Jelly and Boba 🐈❤️


Software Engineering 🌐

Announcing TypeScript 4.0 Beta – Daniel Rosenwasser

Even though the major version changed from 3 to 4, there are no significant breaking changes. There are additions to type systems like variadic tuple types and labeled tuple elements, and developer experience improvements. I especially look forward to partial editing mode since my project already takes too long to start up.

Variadic tuple types – Anders Hejlsberg

If you were intimidated by the term “variadic tuple types,” I was, too. This PR to Typescript goes deep into what variadic tuple types are, and how it will work in Typescript. In short,

A variadic element is a spread element of the form …T, where T is a generic type constrained to any array or tuple type (specifically, any type that is assignable to readonly any[]).

People ❤

Written communication is remote work super power – Snir David

We need to consider several factors when we think about asynchronous communication: first, the purpose: are we transferring knowledge or creating new? Second, the scope: are we communicating within or without our team? Third, the feasibility: can our team meet synchronously? Depending on the answers, synchronous communication will be preferable. But I agree with the article that we need to choose asynchronous communication more than we currently do.

Reflections on Being a Female Founder – Tracy Young

Before joining Netflix, I had an offer from PlanGrid, the company she founded. So I watched her TechCrunch interview from 2015, in which she disappointed with her lack of gender sensitivity. I love that she courageously, and publicly admits her faults, and shares her learnings.

Business 💸

The Global AI Talent Tracker – MacroPolo

A quantitative argument for allowing more mobility across borders with a beautiful visualization. More relevant now than ever, in the light of the US ban on working visas.

I just hit $100,000/yr on GitHub Sponsors 🎉 – Caleb Porzio

This article is a cool story of how a freelancing developer grew his Github Sponsorship. Most of the sponsorship came from private videos, which means his value proposition was more similar to a paid newsletter or a paid podcast than Patreon.

Good Questions, Real Answers: How Does Facebook Use Machine Learning to Deliver Ads? – Facebook

A short but helpful article on how Facebook’s programmatic ad bidding works.


Young Reacts #71 – Crank.js, blurhash, Seniorless, and more

One benefit of working from home and the resulting lost sense of time is that every weekend comes very fast.

Photo by Djim Loic on Unsplash


Software Engineering 🌐

Introducing Crank – Brian Kim

The entire React lifecycle […] could be expressed within a single async generator function.

An interesting experiment to move away from pure functions in React framework to side effects with async functions in Javascript. It was quite astounding to see how component lifecycle methods nicely map to a generator function.

blurhash: A very compact representation of a placeholder for an image. – Dag Ågren

blurhash is easily the most exciting library I found this week. With just a pre-calculated short string (20-30 characters), you can create a beautiful placeholder image.

Versioning fields in GraphQL – Leonardo Losoviz

We can create our field with an argument called version, through which we specify which version of the field to use.

I never thought of versioning subsections of a schema with a version argument. However, I still expect that the work of defining a contract, communicating changes, and program-managing the changes will not go away. Because those are human problems, not technical ones.

Typing functions in TypeScript – Axel Rauschmayer & Augmenting Interfaces in the Global Scope in TypeScript – Marius Schulz

These two Typescript articles taught me two things I always get confused about: How to overload functions, and how to type the global scope in a type-safe way.

People

Seniorless — 5 Reasons You Should Hire More Juniors – Gabriel Grinberg

I was in similar shoes when I was leading a team at a startup. I didn’t have the budget to hire senior engineers, so I instead turned to junior engineers and developed them. Contrarily, I concluded after two years, if your project is time-sensitive, and needs less uncertainty, not more, you should consolidate the headcounts and hire fewer, more senior engineers.

Carta’s covid-19 layoff – Henry Ward

I don’t want to celebrate this article since a lot of people just lost their jobs. However, I do appreciate a CEO of a company taking full responsibility for the decision to lay off.

Business 💸

COVID-19 Global Impact Charts – Luke Wroblewski

An eyeopening set of graphics showing how our society has changed because of the pandemic. Would you have guessed we sleep more now that we don’t need to commute?

The third wave of open source migration – Donald Fischer

for any technology-driven organization that hasn’t embraced the reality that the modern application development platform is a polyglot mix of open source languages, frameworks, and packages: the time is now.

The calculations will differ by organization. The crisis probably put cost-saving at a higher priority. Eventually, it comes down to build vs. buy and tradeoffs between paying the price now vs. later, and immediate benefit vs. future flexibility.

Young Reacts #58

I joined a meeting this week where we discussed how to handle error states. And it was hands down the best meeting experience. The facilitator sent an agenda long before, took notes of discussions, spared plenty of time to create action items, and shared the summary of the meeting on the same day. It was a rare find even at Netflix. This meeting will be my standard going forward.

Photo by You X Ventures on Unsplash

People ❤

How to manage a program in a product-mode organization

Even in a transparent workplace like Netflix, achieving alignment across different teams is hard. The company and the orgs have a high-level strategy; but each team plans its roadmap autonomously, which often doesn’t consider interdependencies. This article suggests that cross-functional projects call for different organizing methods.

Software Engineering 

Code-wise, cloud-foolish

When I think about lock-ins, I often think about vendor lock-ins, such as AWS or JIRA. But my opensource tech stacks have lock-in effects as well (since I can’t easily change my code), and I should be mindful of it.

Copying data is wasteful, mutating data is dangerous

An enlightening thought that a function interface needs to be immutable, but the internal can use mutation for optimization.

A short Twitter Thread on Typescript type arguments

These couple of tweets explained so much about how Typescript’s type arguments work.

react-error-boundary

A handy library if you want to use hooks and error boundaries at the same time.

Business 

The ‘No Code’ Delusion

No Code is trending in the tech industry at the moment. Engineers are expensive, and products take too long to build. The article reacts to the movement. I liked the parallel between this generation of tools and the old drag-and-drop website editors, which failed to replace all UI engineers.

Part 2: Typescript+Redux Best Practice at Vingle

Part 1: History of Redux State Management at Vingle

In this part two, I am going to describe our team’s current best practices to make Typescript work for you when working with Redux.

  • Creating Type-safe Actions and Reducers
  • Properly typing Redux Container

Creating Type-safe Actions and Reducers

Considering how reducers are just simple functions that accept two arguments, you would expect Typescript to work well with those two. States do. But actions, because dispatch accepts any types of arguments, cannot be typed safely without developers’ involvement. If you don’t type your actions, your reducer will end up in the not-so-ideal state:

function reducer(state = INITIAL_STATE, action: Redux.Action) {
switch (action.type) {
case ActionTypes.FETCH_USER: {
// simple case
return {
state,
userId: (action.payload as any).userId,
};
}
default: {
return state;
}
}
}
view raw type-safe-reducer.ts hosted with ❤ by GitHub

You can catch some of type errors with unit tests, but you will miss some properties and lose easy refactoring provided by Typescript. To acheive type-safety before Typescript 2.8, you could use string enum:

enum ActionTypes {
FETCH_USER = "FETCH_USER",
}
interface IFetchUserAction {
type: ActionTypes.FETCH_USER;
payload: { userId: string }
}
interface IOtherAction {
type: "____________________";
}
type Actions = IFetchUserAction | IOtherAction;
function fetchUser(userId: string): IFetchUserAction {
return {
type: ActionTypes.FETCH_USER,
payload: {
userId,
}
};
}
function reducer(
state = INITIAL_STATE,
action: Actions,
): IState {
switch (action.type) {
case ActionTypes.FETCH_USER: {
// in this closure, Typescript knows that action is of interface IFetchUserAction, thanks to enum ActionTypes.
return {
state,
userId: action.payload.userId,
};
}
default: {
return state
}
}

IOtherAction is needed so that Typescript won’t complain about default case in switch statement (that is, exhaustiveness checking). This works OK if you ignore the fact that there are essentially two duplicate type definitions in your action interfaces, and action creators. Starting with Typescript 2.8, you can use ReturnType to remove action interfaces. The code below is our way to type actions and reducers.

import { ActionCreatorsMapObject } from "redux";
// interface ActionCreatorsMapObject {
// [key: string]: ActionCreator<any>;
// }
type ActionUnion<T extends ActionCreatorsMapObject> = ReturnType<
T[keyof T]
>;
enum ActionTypes {
FETCH_USER = "FETCH_USER",
}
function createAction<T extends { type: ActionTypes }>(d: T): T {
return d;
}
export const ActionCreators = {
fetchUser(payload: {userId: string}) =>
createAction({type: ActionTypes.FETCH_USER, payload}),
}
type Actions = ActionUnion<typeof ActionCreators>;
function reducer(
state = INITIAL_STATE,
action: Actions,
): IState {
switch (action.type) {
case ActionTypes.FETCH_USER: {
// in this closure, Typescript knows that action is of ActionCreators.fetchUser's ReturnType.
return {
state,
userId: action.payload.userId,
};
}
default: {
return state
}
}

Typing Redux Container components

Typing Redux container components correctly is important to use, and test the components correctly. Before our team learned how to type components, we ended up with tests like this:

const Container = (props: { data: any; dispatch: Dispatch<any> }) => {
// render something and do something useful
return <div />;
};
const ConnectedContainer = connect()(Container);
describe("", () => {
let wrapper: ReactWrapper;
beforeEach(() => {
const store = mockStore(state);
wrapper = mount(<ConnectedContainer data dispatch={store.dispatch} />, { store });
});
});

So let’s dive in.

Before you try to type Redux container components properly, you need to understand the type definition of connect. Carefully read the code below I quoted from Redux type definition (comments are mine). The definition uses a lot of type overloading but I will go through some cases to help you understand what exactly goes on.

Please note that the definitions below are from @types/react-redux@5.0.19.

When you don’t pass in any argument to connect

This is when you only need dispatch inside your container.

const Container = (props: { data: any; dispatch: Dispatch<any> }) => {
// render something and do something useful
return <div />;
};
export default connect()(Container);
view raw simple-container.ts hosted with ❤ by GitHub

As there are no arguments to connect, all connect will do is to inject dispatch<any> into props.

When you pass in mapStateToProps to connect

If you want to map only state to props, say for render only components, you

type SearchData = { query: string };
type AppState = {
searchData: SearchData;
};
type Props = { query: string; data: any; dispatch: Dispatch<any> };
function mapStateToProps(state: AppState) {
return {
query: state.searchData.query,
};
}
const Container = (_props: Props) => {
// render something and do something useful
return <div />;
};
const A = connect(mapStateToProps)(Container);
<A data />; // this is valid
<A data dispatch={store.dispatch} />; // this isn't valid
view raw mapped-container.ts hosted with ❤ by GitHub

It almost looks like a magic as Redux type definition does a lot of heavy lifting for us. Let’s examine what actually happens inside the code above.

interface Connect {
<TStateProps = {}, no_dispatch = {}, TOwnProps = {}, State = {}>(
mapStateToProps: MapStateToPropsParam<TStateProps, TOwnProps, State>,
): InferableComponentEnhancerWithProps<
TStateProps & DispatchProp<any> & TOwnProps,
TOwnProps
>;
}

This^ connect definition is the overloaded type definition used. In the definition, mapStateToProps is expanded to

(initialState: State, ownProps: TOwnProps) => (
state: State,
ownProps: TOwnProps,
) => TStateProps;

So Typescript will infer TStateProps, and State to be {query: string}, and AppState from the argument mapStateToPropsInferableComponentEnhancerWithProps is expanded to

<P extends (TStateProps & DispatchProp<any> & TOwnProps)>(component: Component<P>): ComponentClass<Omit<P, keyof (TStateProps & DispatchProp<any> & TOwnProps)> & TOwnProps> & {WrappedComponent: Component<P>}
view raw container-type.ts hosted with ❤ by GitHub

And Typescript will infer P to be Props, and check whether the container component’s props is larger than the union of TStatePropsDispatchProp<any>, and TOwnProps.

If I put the logic above into code, it looks like the following:

type TStateProps = ReturnType<typeof mapStateToProps>;
type TOwnProps = Omit<Props, keyof TStateProps | keyof DispatchProp<any>>; // this results in { data: any }. But this isn't necessary and you can use {} without a problem.
const B = connect<TStateProps, {}, TOwnProps, AppState>(mapStateToProps)(
Container,
);
<B data />; // this is valid
<B data dispatch={store.dispatch} />; // this isn't valid
view raw summary.ts hosted with ❤ by GitHub

When you pass in both mapStateToProps and mapDispatchToProps to connect

This isn’t hard to understand once you understood how Redux type definition handles mapStateToPropsmapDispatchToProps is treated like mapStateToProps. For your reference, I included the overloaded type below.

interface Connect {
<TStateProps = {}, TDispatchProps = {}, TOwnProps = {}, State = {}>(
mapStateToProps: MapStateToPropsParam<TStateProps, TOwnProps, State>,
mapDispatchToProps: MapDispatchToPropsParam<TDispatchProps, TOwnProps>,
): InferableComponentEnhancerWithProps<
TStateProps & TDispatchProps & TOwnProps,
TOwnProps
>;
}
view raw both-container.ts hosted with ❤ by GitHub

When you also pass in mergeProps

This is also rather straightforward. Instead of merging TStatePropsTDispatchProps, and TOwnProps naively for the component definition, Connect will now depend on mergeProps to merge these props. The only additional check, (or inference) is whether mergeProps is of type (stateProps: TStateProps, dispatchProps: TDispatchProps, ownProps: TOwnProps): TMergedProps;.

What this means

First of all, congratulations on getting through all these different types! Now you get how Connect works. But, it turns out you don’t need to type things directly when you use Redux’s Connect. However, other HOC’s definitions will vary, and you will need to learn how their type systems work.

Extracredit (Typescript tips not related to Redux)

Know your types in React

Knowing React types helps your code to work with React seamlessly. Here is the usual go-to list for us.

React.Component<P, S>
React.StatelessComponent<P>
React.ReactElement = instantiated React Component
React.ReactNode = React.ReactElement + Renderable primitive types (object is not valid). `children` has this type
React.CSSProperties
React.ReactEventHandler
React.<Input>Event
React.HTMLProps<ElementType> = Used to extend your component props. Ex) TOwnProps & React.HTMLProps<HTMLDivElment>

How to type HOCs that inject props

The following code is an excerpt from react-intl. This type definition is straight-forward to set up, but expects the users of the library to know which props are injected into.

interface InjectedIntlProps {
intl: InjectedIntl;
}
function injectIntl<P>(
component: ComponentConstructor<P & InjectedIntlProps>,
options?: InjectIntlConfig,
): React.ComponentClass<P> & {
WrappedComponent: ComponentConstructor<P & InjectedIntlProps>;
};
// actual usage
interface IProps {
flag: boolean;
}
class Toast extends React.PureComponent<IProps & InjectedIntlProps> {
}
export default injectIntl<IProps>(Toast);
view raw hoc-types.ts hosted with ❤ by GitHub

Use Ambient Types to simplify your dependencies

This is an easy-to-miss option when you first start using Typescript. You should use typeRoots option to avoid adding unnecessary dependencies.

Afterword

As we develop, and maintain our React apps, we have encountered many bugs. Based on our experience, the harder-to-track, and more critical bugs often stemmed from typeless part of the code. That is why we are determined to type things both comprehensively, and correctly. This isn’t the farthest we can go with Typescript, but this is where we are at, and I hope this article has helped you understand Typescript and Redux more deeply.

Part 1: History of Redux State Management at Vingle

Part 2: Typescript+Redux Best Practice at Vingle

This post is a repost of my post at Vingle Tech Blog.

In this two-part post, I am going to go over the different flavors of Redux state management at Vingle and our thought process behind each iterations we went through over the last year and half. I hope this post guide how you structure your Redux states.

Genesis: Redux + Immutable.Map

My team chose React to create a small-scale mobile marketing website as a learning experiment. Our main project, at the time, was based on Rails, and Angular 1, and we were separating web applications from Rails to simplify, and speed up our deployment process. That meant we had to create everything from scratch: a new build pipeline, a new webpack configuration, while learning about the vast React ecosystem.

We heard that Redux simplifies debugging application states greatly, and, with the nightmarish memories of debugging Angular 1’s watchers, chose to adopt Redux. We also learned a bit about shouldComponentUpdate and React’s component lifecycle, and wanted to have an immutable state. I was already familiar with high-order immutable objects from my previous work (this), so Immutable.js was an obvious choice.

In the end, we have Redux setup looking like this:

import { Map } from "immutable";
// reducers
const INITIAL_STATE = Map({ post: null, isLoading: false });
function postReducer(state = INITIAL_STATE, action) {
switch (action.type) {
case "FETCHED": {
return state.withMutations(currentState =>
currentState.set("post", action.payload.post).set("isLoading", false),
);
}
default: {
return state;
}
}
}
// action creators
function fetchedPost(post) {
return {
type: "FETCHED",
payload: {
post,
},
};
}
view raw genesis.js hosted with ❤ by GitHub

1st Iteration: Typescript + Redux + Immutable.Map

Once we have gotten more used to React, and Redux, and proven that we could develop new features much faster on the mobile page, we started migrating our main web application to React. But unlike the proof-of-concept mobile page, this app would have dozens of routes and reducers, and much more complex components, so we chose to use Typescript for this app.

Unfortunately, Immutable.Map with different types of values (number, boolean, other Maps, or Lists, for example) does not play well with Typescript. The following is a Typescript definition of Immutable.Map:

interface Keyed<K, V> extends Collection<K, V>, Iterable.Keyed<K, V> {}
interface Map<K, V> extends Keyed<K, V> {
set(key: K, value: V): Map<K, V>;
setIn(keyPath: Array<any>, value: any): Map<K, V>;
}
view raw first-iteration-1.ts hosted with ❤ by GitHub

As you can see, there isn’t a good way to specify different types of a Immutable.Map’s values. So we ended up doing this hacky workaround.

// scaffolding
interface IPostStateImmutable {
get(key: "post"): IPostImmutable | null; // IPostImmutable is also another hacky interface like IPostStateImmutable.
get(key: "isLoading"): boolean;
set(key: "post", value: IPostImmutable | null): IPostStateImmutable;
set(key: "isLoading", value: boolean): IPostStateImmutable;
withMutations(
mutator: (mutable: IPostStateImmutable) => IPostStateImmutable,
): IPostStateImmutable;
}
// reducers
const INITIAL_STATE: IPostStateImmutable = Map({
post: null,
isLoading: false,
});
function postReducer(
state: IPostStateImmutable = INITIAL_STATE,
action,
): IPostStateImmutable {
switch (action.type) {
case "FETCHED": {
return state.withMutations(currentState =>
currentState.set("post", action.payload.post).set("isLoading", false),
);
}
case "UPDATED_TITLE": {
return state.setIn(["post", "title"], action.payload.title);
}
default: {
return state;
}
}
}
// actions stay the same
view raw first-iteration-2.ts hosted with ❤ by GitHub

Needless to say, this pattern is painful to maintain, and hard to guarantee correctness. Typescript got in the way rather than helping us.

2nd Iteration: Typescript + Redux + Immutable.Record

So we looked for a better way to tie Typescript and Immutable.js together. Then we found that there was another Immutable class called Immutable.Record and a library called typed-immutable-record. With the library, we created a type-safe Immutable Record:

import { TypedRecord, recordify } from "typed-immutable-record";
// scaffolding
interface IPostState {
post: IPost | null;
isLoading: boolean;
}
interface IPostStateRecordPart {
post: IPostRecord; // this interface is created in a similar fashion.
isLoading: boolean;
}
interface IPostStateRecord
extends TypedRecord<IPostStateRecordPart>,
IPostStateRecord {}
function recordifyPostState(plainState: IPostState): IPostStateRecord {
return recordify<IPostStateRecordPart, IPostStateRecord>({
post: plainState.post
? recordify<IPostRecordPart, IPostRecord>(plainState.post)
: null,
isLoading: plainState.isLoading,
});
}
// reducers
const INITIAL_STATE: IPostStateRecord = recordifyPostState({
post: null,
isLoading: false,
});
function postReducer(
state: IPostStateRecord = INITIAL_STATE,
action,
): IPostStateRecord {
switch (action.type) {
case "FETCHED": {
return state.withMutations(currentState =>
currentState.set("post", action.payload.post).set("isLoading", false),
);
}
case "UPDATED_TITLE": {
return state.setIn(["post", "title"], action.payload.title);
}
default: {
return state;
}
}
}
view raw second-iteration.ts hosted with ❤ by GitHub

It took some time for us to understand how to scaffold Record interfaces correctly but we managed to create type-safe redux states with both dot notations, and helper methods like setIn or withMuationsHowever, as you can see from the code above, we had to create a large number of interfaces, especially when our states were deeply nested. Once we got the pattern down, it wasn’t difficult to follow the pattern but it was a lot of work which disincentivized our team to create smaller, and isolated reducers. But we didn’t know any better, so we carried on.

3rd Iteration: Typescript + Redux + Typescript Readonly Interfaces

During a random conversation with an engineer at another startup, I learned about readonly properties in Typescript, and realized those properties could replace Immutable.js completely.

// scaffolding
interface IPostState
extends Readonly<{
post: IPost | null;
isLoading: boolean;
}> {} // this has to be a Readonly interface as well.
// reducers
const INITIAL_STATE: IPostState = {
post: null,
isLoading: false,
};
function postReducer(state: IPostState = INITIAL_STATE, action): IPostState {
switch (action.type) {
case "FETCHED": {
return {
post: action.payload.post,
isLoading: false,
};
}
case "UPDATED_TITLE": {
return {
state,
post: {
state.post,
title: action.payload.title,
},
};
}
default: {
return state;
}
}
}
view raw third-iteration-1.ts hosted with ❤ by GitHub

By using Readonly interfaces, the scaffolding is reduced to a quarter by removing RecordPartRecord, and recordify. However, there is a problem with this approach when you need to update deeply; the case above UPDATED_TITLE is such an example. During the conversion, we had some codes go out of hand like this:

return {
state,
post: {
state.post,
author: {
state.post.author,
relation: {
state.post.author.relation,
following: true,
},
},
},
};

4th Iteration: Typescript + Redux + Typescript Readonly Interfaces + Normalizr

We could solve this problem by adopting a deep merge library, but we feared that those libraries may not be type-safe. After giving some thoughts, we determined that the real problem was with the deeply nested structures of our states and planned to flatten the states by normalizing. Of the two popular normalizing libraries, redux-orm, and normalizr, we chose the latter for its simplicity.

Our final, and current version of redux looks like the following:

// post reducer
interface IPostState
extends Readonly<{
postId: number | null;
isLoading: boolean;
}> {}
const INITIAL_STATE: IPostState = {
postId: null,
isLoading: false,
};
function postReducer(state: IPostState = INITIAL_STATE, action): IPostState {
switch (action.type) {
case "FETCHED": {
return {
postId: action.payload.postId,
isLoading: false,
};
}
default: {
return state;
}
}
}
// normalized entity reducer
interface IEntityState
extends Readonly<{
posts: {
[postId: number]: INormalizedPost;
};
}> {}
function entityReducer(
state: IEntityState = { posts: {} },
action,
): IPostState {
switch (action.type) {
case "ADD_ENTITIES": {
return {
state,
posts: {
state.posts,
entities.posts,
},
};
}
case "UPDATED_TITLE": {
const postToUpdate = state.posts[action.payload.postId];
if (!postToUpdate) {
return state;
}
return {
state,
posts: {
state.posts,
[action.payload.postId]: {
postToUpdate,
title: action.payload.title,
},
},
};
}
default: {
return state;
}
}
}
// action creators
function fetchedPost(postId: number) {
return {
type: "FETCHED",
payload: {
postId,
},
};
}
function addEntities(entities: Partial<IEntityState>) {
return {
type: "ADD_ENTITIES",
payload: {
entities,
},
};
}
// container component
function mapStateToProps(state: IAppState, _routeProps: any) {
return {
post: denormalize(state.postState.post, postEntity, state.entities),
};
}
view raw fourth-iteration.ts hosted with ❤ by GitHub

Afterword

When I look back, part of me regret that we didn’t do more research which could have saved a lot of time; this collection of redux-related libraries would have been helpful, and normalizing is already in official Redux documention. However, part of me also feel like we would have never appreciated the utility of these libraries and techniques because we didn’t know the downsides of not using those libraries and techniques. And that is why I wrote this post; I hope you understand what problems lie ahead and save yourself some time.