Skip to content

Instantly share code, notes, and snippets.

@dpoindexter
Created October 6, 2015 15:43
Show Gist options
  • Save dpoindexter/fdc599005c260296e881 to your computer and use it in GitHub Desktop.
Save dpoindexter/fdc599005c260296e881 to your computer and use it in GitHub Desktop.
Building React apps with Omniscient

Building React apps with Omniscient Patterns and practices

First, if you haven't read these article, please do so.

  1. Thinking in React (https://facebook.github.io/react/docs/thinking-in-react.html) The main takeaway here is how to structure your app as a tree of components. Once you're used to breaking down a feature into sub-components, the rest of this starts to make a whole lot more sense.

  2. Simpler UI Reasoning with Unidirectional Data Flow (http://omniscientjs.github.io/guides/01-simpler-ui-reasoning-with-unidirectional/) This is a conceptual overview of how Omniscient helps us manage state and complexity when building React apps. Because there are a lot of new concepts here, it's really important to understand the "why" behind our approach.

  3. Basic Omniscient Tutorial (http://omniscientjs.github.io/tutorials/02-editing-basic-tutorial-creating-list-with-live-filtering-jsx-edition/) This tutorial introduces the basic pattern of constructing components, using cursors to pass props to the components, and making updates in response to user actions.

Our architecture

  1. Top-level state Every page has one data object that describes the current state of the UI, and any data that it contains. You can think of this as being the model in MVC: It exactly represents all of the data in your program that is subject to change. Keeping everything in one data object means we have a single source of truth, that can be easily saved and centrally updated.

A good way to think about top-level state is, "if the user accidentally refreshes the page, what would we have to save in order for them not to lose their work?" This usually includes things like client-side navigation routes and form data. It also means there will be pieces of transient state that a user wouldn't expect to be saved -- an open dropdown menu, for example.

  1. Root component Just like there's one top-level state, our React applications will have a single component that is the point of entry. It doesn't have its own state: it's re-rendered every time any part of the top-level state changes, and receives the entire top-level state as a prop. It doesn't really have any responsibility other than to compose the high-level components that actually represent the functionality of the application.

  2. Components The simplest way to think about components is that they are "pure" functions -- that is, given the same arguments they will always return the same results -- with the signature (props) => virtualHtml. They simply take in state data in the form of props, and predictably return a set of virtual DOM nodes.

When you build an app, what you're actually doing is making a tree -- just like a plain HTML page -- by composing together components. The closer to the root component you are, the more abstract a component's purpose will be. The lower you get, the more concrete its purpose will be. It's a good rule of thumb in software development not to mix levels of abstraction. So if you find yourself writing a bunch of, say, HTML layout code in a high-level component, think about how you might be able to redefine that work as lower-level components instead.

Components are rendered based on the data contained in slices of the top-level state. At each level, the parent component passes its child components the smallest slice of data that the child needs to be able to render. Omniscient's cursor pattern helps us by doing all of the book-keeping for what updated where: when a component replaces something in its small slice of data, Omniscient knows where to make the replacement in the top-level state, and triggers the root component to re-render with the new top-level state.

Thinking about components

There are two kinds of components: General-purpose, and application-specific.

General-purpose components capture a common piece of functionality -- for example, displaying a sorted list of items -- that isn't specific to any one uShip feature.

Application-specific components are by their nature closely tied to a particular product feature, and will only be used where that product feature itself is used. For example, a set of controls used to select a listing's service type.

The important thing to recognize is that application-specific components are composed of general-purpose components. A lot of the work we're doing is simply a question of recognizing a repeated pattern, and abstracting it into a general-purpose component. As we build up our library of abstractions, making new features gets a lot easier. Features end up becoming mostly new combinations of existing solutions.

Naming and organization

Generally, each component should be a file. Components are upper-case (this is enforced by React, actually), and so should the file names. It's okay for these files to contain sub-components, as long as those sub-components won't be used on their own, and aren't exposed by the file. They're the equivalent of private methods.

  • General-purpose components should live somewhere in the /components namespace. Organize more granularly as necessary; something like /components/forms/controls/TextBox.js probably makes sense.

  • Application-specific components should live in the namespace of the application they're part of, e.g. /pricingcontext/.

  • Props are super important -- they're basically the public interface that a component exposes to the world. Just as with function arguments, a component that takes in a laundry list of different props is probably trying to do too many things.

  • Name props as descriptively as possible. Avoid generic suffixes like "-Data" unless you have a case where that's a useful disambiguation. Generally, props will be one of two things:

-- A cursor. Name this prop using the Hungarian notation "c", e.g. "cRates" or "cShipmentItem".

-- A value type, like a string or boolean. Usually exposed for the parent to control how a child component displays, not as part of the top-level state, e.g. . Name these without any prefix.

Communicating with parents

-- A callback function. Usually passed in from a parent that needs to know when a particular action happens on a child, e.g.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment