sleepy

TOPICS

IntroductionReact (with MobX)ArchitectureModelsNetworkingData TransformersStoresViews & View Models

React (with MobX)

Why do we need MobX?

When using MVVM, we need a way to push updates to view models from our model and the UI from our view models when something changes. MobX is a library that does just this, allows us to set up observable or computed properties that trigger other things (like re rendering or re computing) to happen elsewhere in our code. MobX is inspired by reactive programming principles so it's a simple and lightweight library that we use to trigger other areas of our app. MobX does not define how we set up our stores, view models, or anything else in our app, it just helps us implement the observable pattern with reactive programming.

If you have another library that does this or your own way of setting up reactive programming, feel free to use that instead! MobX is just my preference when implementing MVVM. MobX specifically is not required.

How do I use this information?

The page is written in a way to start simply, and then add layers of abstraction where it starts to make sense, so some of the things we do at first, will not ultimately be how they end up. We do this gradually to be able to explain why we'd do it at all!

The Weather App

We are building an app that has the following requirements

  • Search for the current temperature by zip code and display the temperature in Celsius and Fahrenheit, as well as the city name. This does not need to be remembered for the duration of the session.
  • Search for the current weather using the current location and display the temperature in Celsius and Fahrenheit, as well as the city name. Remember this for the duration of the session.

We can keep it simple by using create-react-app with TypeScript and add MobX.

1 2 3 yarn create react-app weather-mvvm-app --template typescript cd weather-mvvm-app yarn add mobx mobx-react

First, Networking

Because this is a real world app that is going to hit the OpenWeather API, we can get that ready first. If you already have a way of setting up your networking layer, feel free to use that, otherwise check out the Networking documentation in the Architecture section on how to do so, and why we abstract it (as opposed to making networking calls directly from our view models). You can also copy the networking layer from my MVVM Web Github. Generate a free API key by creating an account and navigating to the API keys section to generate a key The API docs can be read through here if you're curious, but you don't need to do that for this exercise.

Creating a View and its ViewModel

Let's start with the zip code requirement. We know we need an input, a search button, and a way to display the temperature and city, so let's start with setting up each file, and then move onto the simplest thing which is the temperature label.

I've created a directory called ZipCodeWeatherView which will match the component name and all of the following files will be defined in that directory.

We know we'll need a ViewModel.ts, so let's create that

1 2 // ViewModel.ts class ViewModel {}

Then we will need a View.tsx that will eventually it

1 2 3 4 5 6 7 8 // View.tsx import ViewModel from "./ViewModel"; interface Props { viewModel: ViewModel; } const View = ({ viewModel }: Props) => <div>Hello, world!</div>;

And finally, the provider or binder or "glue" that connects the two and is what all of our other components will use directly (as opposed to the View and ViewModel we just created).

1 2 3 4 5 6 7 8 9 10 // index.ts import ViewModel from "./ViewModel"; import View from "./View"; const ZipCodeWeatherView = () => { const viewModel = new ViewModel(); return <View viewModel={viewModel} />; }; export default ZipCodeWeatherView;

Why do we need three files? Technically these could all be defined in the same file but I prefer to separate by responsibility (the view, the view model, and the binder for the two). Specifically separating the view model and the view helps me mentally and physically separate UI and logic and then for consistency and clarity, I separate the binder as well. Every view that needs to do some logic will follow this pattern. Would it be more convenient to do them all together? Sure, but what's "more convenient" right now can often be in conflict with what's best for the testing or scalability of a codebase.

We will have these three files for all of our components, with the only difference being the name of the component exported from the index.tsx. If it helps you to name them differently (for example ZipCodeWeatherViewModel, ZipCodeWeatherView and ZipCodeWeatherProvider), absolutely feel free to do that as long as you're consistent with naming throughout your app.

App State

It's common to need to hold some data that we fetch from an API in a place that's accessible to multiple screens or views. It's also common to only need some data for a specific page and we can disregard it once the user navigates away. We will cover how to handle both of these situations.

Because our store will be injected into our view models, we'll keep it abstracted with an interface. We'll create a concrete implementation called WeatherStore because it'll store our weather data. It will also take an implementation of our network layer as a dependency in order to be able to access OpenWeather. I'll name the interface WeatherStorable because any implementation is able to store weather data, but feel free to use whatever naming conventions you like (the -able or -ing suffixing I use comes from the Swift naming guidelines if you were curious).

1 2 3 // WeatherStorable.tsx interface WeatherStorable {} export default WeatherStorable;
1 2 3 4 5 6 7 8 9 10 // WeatherStore.ts import WeatherStorable from "./WeatherStorable"; class WeatherStore implements WeatherStorable { private readonly networker: OpenWeatherNetworkable; constructor(networker: OpenWeatherNetworkable) { this.networker = networker; } }

This is the basics of our store with dependencies injected and abstractions ready to go. makeAutoObservable from mobx will make all of our properties observable by default. You can read more about what it does here but if you're unfamiliar with reactive programming, don't worry about it for now.

Let's add the functionality for getting weather by location by zipcode by having a method we can call on the store that will make the API call, unpack the networking request, and return an object we'd like to use. So first, let's define what we'd like to use in our UI. I know OpenWeather returns the temperature in Kelvin, and I'd like to convert the Kelvin to both Celsius and Fahrenheit, and I'd also like to display the city name for the zip code. Let's create a type for that.

1 2 3 4 5 6 7 type Weather = { temperature: { fahrenheit: number; celsius: number; }; city: string; };

Now let's put it all together! First we can define the method in our interface to do this. It can be handy to think about the interfaces first since we don't need to get bogged down with implementation yet. We can just think about it like a black box. We know we need to pass in zip, we know we want a Weather object in return. We can define our interface like so

1 2 3 4 5 6 7 8 9 // WeatherStorable.tsx import Weather from "../models/Weather"; interface WeatherStorable { /** Returns the weather for a zip code. */ getWeatherForZip(zipCode: string): Promise<Weather>; } export default WeatherStorable;