A Year of development with Redux. Part I
Update May 20, 2020: For new code, prefer plain React for state management, via the React context and React Hooks. Is Redux deprecated? No, it isn't. But you probably don't need it, and your code can be simpler and more maintable without out.
I’ve spent the past year working on the primary product of ShakaCode and I’d like to share three biggest insights that I gained during this journey.
The App
We’ve been working on a service called Friends & Guests. It is a vacation rental listing site which puts the relationships between hosts and guests first. It’s like Airbnb or VRBO, with social connections for referrals, guest discovery, and optional privacy.
The app is backed by Rails with react_on_rails gem and UIs are built with React & Redux. We’re not fully a SPA yet, but each big section of the site is JS app and acts like mini-SPA.
On the frontend we’re using:
and bunch of other great libs, non-related to this post.
The section I’ve been mainly working on is the listings management interface, where the host can create a listing, edit it, manage privacy settings, photos and do other listings related things. This part of the JavaScript app consists of 3 sections:
In our app, we try to persist the state on the server as much as we can. For example, when the host started the wizard or edited listing’s draft and left the site for a while — state is stored on the server, and he can get back to the same state anytime he likes. Edit Screen works similar to Medium’s posts: each listing has a draft record, which the host can edit and it won’t go live until host published it.
Insight #1
In a vanilla React app when 2 UI components are located in different parts of UI and need same data, the only way to provide the data for both is to store it in stateful component on top of the render tree and pass it down via props
.
Similar pattern was used to implement Listings UI, but using Redux. We had a number of stores like listingsStore
, photosStore
etc. (To clarify: we have one redux store, but we call slices of the state, that are being transformed by reducers, each a store). Every store contained entities, form state for each entity, statuses (e.g. isProcessing
) etc. From the store, data is combined via selectors and passed down to components — right from the top of the render tree. Like this:
Oh my, that was a bad idea! The render tree was nailed by data props and action creators. Every keystroke in the form field was causing re-render of the whole tree. In addition to that, there were debounced updates to persist input data on the server. The waterfalls of props and re-rendering resulted in a huge performance issue.
To solve this we reconsidered the vanilla React pattern and used features of Redux. Instead of connecting view layer to the state at the top of the UI render tree, we removed top level containers and connected each interactive element (e.g. text input, publish button) or element, that needs data, to Redux state on its own. Thus most of the containers appeared at the tips of the render tree. Like this:
We split our stores on 2 type:
- data stores
- view layer stores
Data stores don’t know anything about UI. They hold only persisted data, and they provide the data to many connected view layer components via their selectors. Basically, it’s our database on the client.
Usually, it’s shaped like this:
const dataStore = Map({
index: Set([1, 2, 3]), // `Set` or `OrderedSet` of `ids`
entities: Map({
1: Map({ id: 1, … }),
2: Map({ id: 2, … }),
3: Map({ id: 3, … }),
}),
});
🔥 Tip: Normalize your ids
If you use
Immutable
,normalizr
and integer serverid
s, then make the type of theseid
s consistent across the client side app to deal only with numeric values.
- In selectors, convert stringified
id
(from url params) to int. Thus, the view layer will receive numeric values.- Create a wrapper for the
normalize()
method, which converts stringified keys of immutableMap
to ints. Thus, you can safely usestate.getIn(['entities', entityId])
whereentityId
is an int.
For each interactive element in the UI we created its own reducer, selectors and / or set of action creators. And finally this element was placed in its own container, where it connects to Redux store.
With this change each form field keystroke will cause a re-render to only a tiny bit of the UI. At the same time, we are able to share the state of the form with others interested in these updated components to any part of UI. We just need to connect
those container components to the store and fetch the required data via selectors.
Keep connected parts small to avoid pernicious coupling
Rule of Thumb: if the distance between the point where prop is being passed and where it’s actually being used is more than 2 components deep, then consider changing the setup of containers and dumb components.
This pattern makes UI code highly maintainable as each connected part is small and exists in isolation: it can be easily changed, moved or deleted with very low risks to break anything.
🔥 Tip: Scope stores
Use
combineReducers
to scope your stores and keep your state (& Redux DevTools) organized. Keep the one data scope and one UX scope per app section:
state:
data:
listingsStore
photosStore
…
listingsIndex:
thumbnailStore
publishButtonStore
…
listingEdit:
titleFormStore
descriptionFormStore
…
This change greatly improved a performance of the app but introduced some new issues, which are the subject of Part II.
Stay tuned!
A Year of development with Redux. Part I