Skip to main

Keep your concerns separate

Improve your React app architecture with this simple trick

I love GraphQL, and I love Apollo. So much so, that in one of my React Native projects I decided to make Apollo the single source of truth. I mean, isn’t that a great idea – having Apollo’s useQuery/useMutation interface fetch and manipulate both remote data and data from a local store? Well, it turned out it isn’t. Take a look at the following code. As you can see, it uses redux to retrieve data from the local store and update it, and an Apollo query to fetch the remote data.


const Dogs = (props) => {
const [someInputValue, setSomeInputValue] = useState('')

const { loading, error, data } = useQuery(GET_DOGS)

const onSomeInputValueChange = (event) => {
  setSomeInputValue(event.target.value)
}

const onDogSelected = (e) => {
  someBusinessLogic(someInputValue)

  props.setPersistedDog(e.target.value)
}

if (loading) return 'Loading...'
if (error) return `Error! ${error.message}`

return (
  <div>
    <h3>{`Persisted dog breed: ${props.persistedDog}`}</h3>
    <input placeholder='some input' value={someInputValue} onChange={onSomeInputValueChange} />
    <select name='dog' onChange={onDogSelected}>
      {data.dogs.map((dog) => (
        <option key={dog.id} value={dog.breed}>
          {dog.breed}
        </option>
      ))}
    </select>
  </div>
)
}

function mapStateToProps(state) {
return {
  persistedDog: state.persistedDog,
}
}

function mapDispatchToProps(dispatch) {
return {
  setPersistedDog: (requested) => {
    dispatch(setPersistedDog(requested))
  },
}
}

export default connect(mapStateToProps, mapDispatchToProps)(Dogs)

Here’s what I decided to do. Using Apollo’s local resolvers (now a deprecated feature), I made it handle both local store reads and updates. The component became much nicer to look at.


const Dogs = () => {
const [someInputValue, setSomeInputValue] = useState('')

const { loading, error, data } = useQuery(GET_DOGS)
const persistedDogData = useQuery(GET_PERSISTED_DOG)
const [setPersistedDogMutation] = useMutation(SET_PERSISTED_DOG)

const onSomeInputValueChange = (event) => {
  setSomeInputValue(event.target.value)
}

const onDogSelected = (e) => {
  someBusinessLogic(someInputValue)

  setPersistedDogMutation({ variables: { persistedDog: e.target.value } })
}

if (loading) return 'Loading...'
if (error) return `Error! ${error.message}`

return (
  <div>
    <h3>{`Persisted dog breed: ${persistedDogData.data.persistedDog}`}</h3>
    <input placeholder='some input' value={someInputValue} onChange={onSomeInputValueChange} />
    <select name='dog' onChange={onDogSelected}>
      {data.dogs.map((dog) => (
        <option key={dog.id} value={dog.breed}>
          {dog.breed}
        </option>
      ))}
    </select>
  </div>
)
}

export default Dogs

Whether the data is remote or local, the only way the component obtains it is via queries, and the only way it updates data is via mutations. The nice thing about this is that we handle local operations in pretty much exactly the same manner as remote ones. The big disadvantage of this approach is that we handle local operations in pretty much exactly the same manner as remote ones… Wait, what? Let’s take a closer look. First, we need to write a GraphQL query for every local operation.


import gql from 'graphql-tag'

export const GET_PERSISTED_DOG = gql`
query GetPersistedDog {
  persistedDog @client
}
`

export const SET_PERSISTED_DOG = gql`
mutation SavePersistedDog($persistedDog: String!) {
  persistedDog(input: { persistedDog: $persistedDog }) @client {
    success
  }
}
`

Next, we need to write a resolver for each operation.


const cache = new InMemoryCache()
const httpLink = new HttpLink({ uri: graphqlEndPoint })

const authLink = setContext((_, { headers }) => {
const token = getToken()
return {
  headers: {
    ...headers,
    'Content-Type': 'application/json',
    'API-Token': token,
  },
}
})

const resolvers = {
Mutation: {
  persistedDog: async (_data, args, _context) => {
    if (args.input.persistedDog === 'null') {
      return { success: true }
    }
    try {
      await localStorage.setItem(KEY, args.input.persistedDog)
    } catch (error) {
      handleError(error)
      return { success: false }
    }
    return { success: true }
  },
},
Query: {
  persistedDog: async (_data, _args, _context) => {
    let persistedDog = null
    try {
      persistedDog = await localStorage.getItem(KEY)
    } catch (error) {
      handleError(error)
    }
    return persistedDog
  },
},
}

export const client = new ApolloClient({
link: authLink.concat(httpLink),
resolvers,
cache,
})

This doesn’t look too bad, you might say. But all of this is for only one property! The situation gets much, much uglier as you add more properties to the local store. The approach is awkward for complex objects, and it’s rigid too: if we need to retrieve only a certain part of an object, or pre-process it in a particular way, we have to write yet another resolver and query. It clearly seems like Apollo wasn’t intended to be used in this manner, because, well, it wasn’t. Unfortunately, the experiment had failed. I had to abandon this endeavor and stick with redux/thunk (which has its own slew of problems).

What is there to be done about this?

All I want is for all data to be fed into the component through a single unified interface. Is there any other way to achieve this? After all, it’s such a simple concept… But wait! React already comes with a single unified interface that provides a component with all the data it needs, out of the box – it’s called props! Let’s take a step back here and look at the problem from a different angle. What we need is to relieve our component of the responsibility of managing the data, or of choosing where to obtain that data from. A component shouldn’t care about how the data was obtained, it should only receive it from a single source. It should only be concerned with rendering the UI. Just like a… view! That’s it! Looked at this way, the compositional nature of React aligns really well with the philosophy of MVC (or MVVM). Some people already prefer to think of React as simply a View in MVC. But how about we split our component into 2 parts – one is going to be the “view”, and the other is gonna play the role of a controller (sort of)? The view shouldn’t be connected to context or any state manager. It should only be concerned with rendering the UI, and it should only receive its data from props. The controller should wrap the view and take care of all the higher-level logic: making requests, navigation, analytics, obtaining data from the state manager, and so on. Views can be used with different controllers, and controllers with different views, but that isn’t the point here. Unlike HOCs, which should be as high-level as possible and reusable with any component, it’s absolutely ok for a controller to work with only one specific view. The main benefit of this pattern is separation of concerns at a lower level than HOCs. And of course, the approach proposed in this post isn’t an exact by-the-book MVC implementation, but rather a pattern inspired by it.

So what does it look like?

Let’s split the code above into view…


const DogsView = (props) => {
const [someInputValue, setSomeInputValue] = useState('')

if (props.loading) return 'Loading...'
if (props.error) return `Error! ${props.error.message}`

const onSomeInputValueChange = (event) => {
  setSomeInputValue(event.target.value)
}

return (
  <div>
    <h3>{`Persisted dog breed: ${props.persistedDog}`}</h3>
    <input placeholder='some input' value={someInputValue} onChange={onSomeInputValueChange} />
    <select
      name='dog'
      onChange={(e) => {
        props.onDogSelected(e.target.value, someInputValue)
      }}
    >
      {props.data.dogs.map((dog) => (
        <option key={dog.id} value={dog.breed}>
          {dog.breed}
        </option>
      ))}
    </select>
  </div>
)
}

export default DogsView

… and controller.


const DogsController = (props) => {
const { loading, error, data } = useQuery(GET_DOGS)

const onDogSelected = (dog, someInputValue) => {
  someBusinessLogic(someInputValue)

  props.setPersistedDog(dog)
}

return (
  <DogsView
    loading={loading}
    error={error}
    data={data}
    persistedDog={props.persistedDog}
    onDogSelected={onDogSelected}
  />
)
}

function mapStateToProps(state) {
return {
  persistedDog: state.persistedDog,
}
}

function mapDispatchToProps(dispatch) {
return {
  setPersistedDog: (requested) => {
    dispatch(setPersistedDog(requested))
  },
}
}

export default connect(mapStateToProps, mapDispatchToProps)(DogsController)

The view doesn’t rely on redux or Apollo, and doesn’t concern itself with processing the data or handling business logic – that’s the responsibility of the controller. The controller, in turn, doesn’t know anything about the low-level details of the UI. Each part is more concise and logically coherent.

Advantages of this approach

The first advantage is, of course, that our component is now much easier to test. Business logic can be tested separately from render logic. There’s no need to mock redux or Apollo when testing the view, and no need to care about rendering details or complex Enzyme configuration when testing the controller. Just take a look at how easy it is to set up a storybook!


const mockProps = {
loading: false,
data: {
  dogs: [
    { id: '1', breed: 'terrier' },
    { id: '2', breed: 'york' },
  ],
},
persistedDog: 'terrier',
onDogSelected: () => undefined,
}

export const DogsViewStory = () => storiesOf('DogsView', module).add('default', () => <DogsView {...mockProps} />)

This pattern helps you improve your app architecture. Separating code into “views” and “controllers” makes it easier to read and understand. You know what to expect from a component before you even open the file, just by looking at its name. The pattern makes you break your code into smaller chunks, which is always good, and it gives you a guideline on how exactly it should be broken. So instead of ending up with components containing both business and render logic, we get smaller blocks, where each block is concise and logically coherent. This approach can also help you follow the Single Responsibility Principle, if you decide to commit to making your code even more granular.

It also makes it much easier for several people to collaborate. One developer can take care of the controller and another of the view, with props acting as the API interface through which their code communicates. One of the many positive side effects of this is that it also forces the developer responsible for a controller to write tests, since they won’t be able to test it by hand through the UI.

Wait…All this sounds kinda familiar

Well, this is probably because you’ve already encountered this pattern. In the React world, it’s more common to see Containers and Presentational components instead of Controllers and Views. However, I’d like to add a few points in defense of the naming convention suggested in this post. In my opinion, it’s better to tie our approach to a well-known, aged, and battle-tested pattern like MVC, rather than to a totally unique and frankly a bit vague concept like Container. After all, that word is reserved for totally different things outside of React. I’m not entirely confident about the View-Controller naming either, since once again, this isn’t real MVC. But the name “Controller” gives a much better idea of what’s inside a file or module than “Container” does. In my most recent project, we had many components with “container” in their names which did not behave like actual containers are supposed to, so the term had already been made meaningless within our codebase. That’s basically what led us to adopt the View-Controller convention, and I kinda prefer it now. But you should use the naming convention you’re most comfortable with.

There are many other similar patterns that use different naming, as Dan Abramov points out in this great post. He also says that his views have evolved since that post was written, and that he doesn’t suggest splitting components like this anymore. His reasoning is absolutely valid, and I agree with him 100%. This approach shouldn’t be forced, and sometimes it does overcomplicate things. At the same time, I still believe this pattern is a pretty powerful tool that you should definitely consider. So, to sum up all of the pros:

  • Data is provided to views via a single interface – props.
  • Separation of concerns at a component level.
  • This pattern is very easy to understand and implement.
  • No need to mock anything other than props when testing views.
  • Setting up and using Storybook, or any other similar tool is much easier.
  • Much smaller components.
  • Components are more reusable.

How to use.

Pretty much all “smart” components should be split into a controller and a view. As a rule of thumb, you should have at least one controller as the topmost component in the hierarchy of a particular page in the case of web, or screen in the case of React Native, right below all the wrappers and HOCs. The controller should render only one component – the view. As I mentioned before, it takes care of API calls, navigation, obtaining data from a global store, firing redux actions, and so on. It should contain conceptually higher-level logic, or state not related to the UI, such as request progress or data validation status. The view, on the other hand, should only care about state and logic tied directly to the UI, such as animations or the current input value. It’s preferable to store input values in the view and only send them to the controller on submit. An understanding of MVC (or MVVM) really helps here.

And there you have it

Of course, this isn’t a one-size-fits-all approach. I don’t think it would add much value on small or simple websites. The purpose of this pattern is to help you manage complexity, so it doesn’t make much sense to use it if a project isn’t too complex, especially considering how much bundle size matters on the web. So, as always, think and decide for yourself.

This post turned out a bit longer than I originally planned. I really hope you found it useful. Please let me know if you have any suggestions or questions.

Thank you very much for reading! 📖

This website uses cookies to persist user choice made on this banner... And that's about it. This website respects your privacy and doesn't store any third-party cookies. All collected analytics data is anonymous. By continuing to use this website, you consent to the use of cookies.