React Native MobX QuickStart

This tutorial isn't suppose to dive into MobX concepts, it's rather practical guide how to setup your first MobX storage.

References

Source code on the GitHub - react-native-mobx-tutorial

I've also created video version on the YouTube

MobX - https://mobx.js.org/

Tutorial template

To follow up the tutorail you can find template project source here.

Install MobX dependecies

For MobX in React Native we have to add following dependecies

# with npm
npm install --save mobx mobx-react-lite

# or with yarn
yarn add mobx mobx-react-lite

Add store classes

MobX suggests to use JavaScript classes to store your data and keep mutation logic near by. It's also possible to use objects but we'll stay with default solution.

Let's start with RootStore class in the ./src/store/index.ts

export class RootStore {
  classes: ClassesStore;

  constructor() {
    this.classes = new ClassesStore();
  }
}

export const rootStore = new RootStore();

As you can see RootStore is just a container for other stores. In our case we have only ClassesStore, but in real world we'll have more. Like

export class RootStore {
  classes: ClassesStore;
  user: UserStore;
  settings: SettingsStore;
  ...
}

Let's also add ClassesStore base to the ./src/store/ClassesStore.ts

export type ClassItemType = {
  id: number;
  title: string;
}

export class ClassesStore {
  items: ClassItemType[] = [];
}

Place store to React Context

To be able easily access store data inside React components we wrap our rootStore instance with React Context.

/** 
 * ./src/store/index.ts 
 **/
import { createContext, useContext } from "react";

...

export const StoreContext = createContext(rootStore);
export const StoreProvider = StoreContext.Provider;
export const useStore = () => useContext(StoreContext);

And wrap our main app component with StoreProvider

/** 
 * ./App.ts 
 **/

...

import { rootStore, StoreProvider } from "./src/store";

export default function App() {
 ...

  return (
    <StoreProvider value={rootStore}>
      <PaperProvider theme={theme}>
        <SafeAreaProvider>
          <Navigation theme={theme} />
          <StatusBar style={isDark ? "light" : "dark"} />
        </SafeAreaProvider>
      </PaperProvider>
    </StoreProvider>
  );
}

Access store data in the component

Now we can easily access our class store items in the component with useStore hook.

/** 
 * ./src/screens/HomeScreen.tsx 
 **/

...

export const HomeScreen = ({navigation}: Props) => {
  const rootStore = useStore();

  return (
    <View style={styles.container}>
      <ScrollView>
        {rootStore.classes.items.map((item) => {
          return (
            <View key={`${item.id}`} style={{marginBottom: 15}}>
              <ClassItem title={item.title} onPress={() => {
                navigation.navigate(routes.EDIT_CLASS, {
                  classItem: item
                })
              }}/>
            </View>
          );
        })}
      </ScrollView>
      ...
    </View>
  );
}

Mutate store logic

And here the thing I like a lot about MobX. We add our mutation logic in the same class we keep our data. Look how we can update and delete ClassesStore items.

/** 
 * ./src/store/ClassesStore.ts
 **/


export class ClassesStore {
  items: ClassItemType[] = [];

  ...

  updateItem(newItem: ClassItemType) {
    const foundItem = this.items.find(item => item.id === newItem.id);
    if (foundItem) {
      this.items = this.items.map(item => {
        if (item.id === newItem.id) {
          return newItem;
        }
        return item;
      })
    } else {
      this.items = [...this.items, newItem];
    }
  }

  deleteItem(itemToRemove: ClassItemType) {
    this.items = this.items.filter(item => item.id !== itemToRemove.id);
  }
}

Use state actions in the EditClassScreen

Simple as it is. Take our item from navigation params or create new if empty. Get classes from useStore hook. On delete function we run classes.deleteItem(item) and on save we do classes.updateItem(newItem).

/** 
 * ./src/screens/EditClassScreen.tsx 
 **/

...

export const EditClassScreen =
  ({navigation, route}: EditClassScreenProps) => {
    const item = route.params?.classItem ?? {
      id: new Date().getTime(),
      title: '',
    };

    const { classes } = useStore();

    ...

    const onDelete = () => {
      classes.deleteItem(item);
      navigation.goBack();
    }

    const onSave = () => {
      if (!title) {
        setError(true);
      } else {
        classes.updateItem({
          id: item.id,
          title,
        })
        navigation.goBack();
      }
    };

    ...
;

Make store observable with MobX

Now if you try to make changes to our items on the edit screen and then back to home you will not see any changes. To make it works we need to integrate MobX into our store. So we make our ClassesStore for observable with makeAutoObservable function

/** 
 * ./src/store/ClassesStore.ts
 **/

...
import { makeAutoObservable } from "mobx";

export class ClassesStore {
  ...

  constructor() {
    makeAutoObservable(this);
  }

  ...
}

To understand better what we've just done I recomment to take a look observable state chapter in the documentation.

Observe store changes in the component

The only thing we need to do to be able observe changes we have to wrap our component with observer function like this

/** 
 * ./src/screens/HomeScreen.tsx 
 **/

...
import { observer } from "mobx-react-lite";
...

export const HomeScreen = observer(({navigation}: Props) => {
  ...
});

Persist store state

Last thing we want to persist our state on the app restart. To do it we'll use mobx-sync. First we're creating trunk with rootStore and AsyncStorage

/** 
 * ./src/store/index.ts 
 **/
...
import { AsyncTrunk } from 'mobx-sync';
import AsyncStorage from '@react-native-async-storage/async-storage';

...

export const trunk = new AsyncTrunk(rootStore, {
  storage: AsyncStorage,
})

...

Then rehydrate trunk before start render our app components

/** 
 * ./App.tsx 
 **/
...
import { rootStore, StoreProvider, trunk } from "./src/store";

export default function App() {
  ...

  useEffect(() => {
    const rehydrate = async () => {
      await trunk.init();
      setIsStoreLoaded(true);
    }
    rehydrate();
  }, []);


  if (!isLoadingComplete || !isStoreLoaded) {
    return (
      <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
        <ActivityIndicator size="large" />
      </View>
    );
  } else {
    return (
      ...
      {/** Our app components tree here */}
      ...
    );
  }
}

Conclusion

This was practical guide how to start with MobX in React Native. We skipped MobX conceps so I defently recommend to check official docs to better understand it.

Don't forget ask your questoins in the comments.

24