+2

Integrating MobX with React: A combination of Mobx and useContext()

In this article, I assume that you have a basic knowledge of React & React Hooks (useContext, createContext, etc).

What problem do we need to solve?

We all want some way to manage state in an application. Nowadays, almost all developers can easily to build a Web Application with Components. For example in React, React Components have their states then we can define and manage these states by using useState() hooks.

import { useState } from "react";

const Button = () => {
  const [toggle, setToggle] = useState<boolean>(false);

  return (
    <div>
      <input
        type="button"
        value="Click me"
        onClick={() => setToggle((prev) => !prev)}
      />
      {toggle && <p>Hello World!</p>}
    </div>
  );
};

export default Button;

Look it's so easy, right? However, let's picture that you are creating a large website with many components, each having numerous states. And sometimes one component needs to share its state with another components or modify state of another component. So now you need a powerful library like MobX, Redux or even a combination of useReducer() and useContext() for managing your app's state.
Now, let's dive into MobX concepts. Curious why I choosing MobX over Redux? Checkout the title of this topic, please 🌝.

What is MobX

Mobx is a powerful and flexible state management library commonly used in React applications. It is designed to simplify the management of state and make it more predictable by using reactive programming principle.
😵‍💫😵‍💫😵‍💫 Reactive Programmning? I'm not sure what that is either, but I'll research and write it in the future blog 🤓🤓🤓

How to integrate MobX with React for state management

Install Mobx 🛠️

In this tutorial, I assumed that you have already known how to create a React Project. So you can follow this page to add MobX to your project. You can choose either mobx-react-lite or mobx-react, the choice is yours. But in this tutorial I prefer to use mobx-react-lite.

Let's start coding now 🥹

Imagine you have an order page that includes your selected items (OrderItem) and the delivery milestones.
So, our state has two main attributes: the list of selected items and the list of delivery milestones.
And now for use MobX for managing our OrderPage, I break it down into three simple steps below:

Basic structure

In this section, I'll show you how to combining MobX and useContext() to manage an OrderPage .

Firstly, define a state class that use will be shared for all components in the order page

import { makeAutoObservable } from "mobx";
import { DeliVeryStatus, OrderItem } from "model/Order";

class OrderState {
    items: OrderItem[] = [];
    deliveryStatus: DeliVeryStatus | undefined = undefined;

    constructor() {
        makeAutoObservable(this);
    }
}

export default OrderState;

Additionally, I have provided the definition for the model OrderItem and the data type of DeliveryStatus here:

export interface Product {
    id: string;
    name: string;
    price: number;
}

export type DeliVeryStatus = "Confirmed" | "Preparing Goods" | "Delivering" | "Delivered" | "Canceled" | "Return Goods";

export interface OrderItem {
    product: Product;
    amount: number;
}

Secondly, create a Provider to supply the context of OrderState to all child components within the OrderPage

import OrderState from "mobx/order";
import { createContext, useContext } from "react";

/**
 * Interface Definition
 * This defines the types for the properties that the OrderPageProvider component will accept.
 */
interface Props {
    children: React.ReactElement;
    state: OrderState;
}

// Create a context to share the OrderState props, yes it's can be undefined
const Context = createContext<OrderState | undefined>(undefined);

/**
 * This is a functional component that acts as a context provider
 * @param props Props
 * @returns It returns a Context.Provider component that provides the state (an instance of OrderState) to its children.
 */
const OrderPageProvider: React.FC<Props> = (props: Props) => {
    return <Context.Provider value={props.state}>{props.children}</Context.Provider>;
};

export default OrderPageProvider;

/**
 * This is a custom hook that makes it easier to access the OrderState from the context.
 * @returns It casts the value of the context to OrderState and returns it
 */
export const useOrderState = () => {
    const state = useContext(Context);
    return state as OrderState;
};

Finally, let's create the order page now

import OrderPageProvider, { useOrderState } from "context/OrderContext";
import { observer } from "mobx-react-lite";
import OrderState from "mobx/order";
import { useState } from "react";

// Don't forget to wrap this function by observer(), or your state won't update when there are any state changes 😁😁😁
const ListItems = observer(() => {
    const state = useOrderState();

    return (
        <div>
            <ul>
                {state.items.map((item) => {
                    return (
                        <li>
                            <div>{item.product.name}</div>
                            <div>{item.amount}</div>
                            <input
                                type="button"
                                value="increase"
                                onClick={() => {
                                    //Handle increasing item
                                }}
                            />
                            <input
                                type="button"
                                value="decrease"
                                onClick={() => {
                                    //Handle decreasing item
                                }}
                            />
                            <input
                                type="button"
                                value="delete"
                                onClick={() => {
                                    //Handle deleting item
                                }}
                            />
                        </li>
                    );
                })}
            </ul>
        </div>
    );
});

// I'm a bit lazy, so you can use a similar mindset to the ListItems component above for coding 😅😅😅
// Don't forget to wrap this function by observer(), or your state won't update when there are any state changes 😁😁😁
const DeliveryMilestone = observer(() => {
    const state = useOrderState();
    return <></>;
});

const OrderPage = () => {
    const [state] = useState(() => new OrderState());
    return (
        <OrderPageProvider state={state}>
            <div>
                <ListItems />
                <DeliveryMilestone />
            </div>
        </OrderPageProvider>
    );
};

export default OrderPage;

Managing Data State

Constuctor

Sometimes, it's necessary to initialize data for all properties in a class. In Mobx, the constructor plays an important role in performing the makeAutoObservable() method when setting the object's state. So, this function can be used to make the properties of an existing object to be observable.

Take a look at the code below:

interface Props {
    orderId: string;
}

class OrderState {
    items: OrderItem[] = [];
    deliveryStatus: DeliVeryStatus | undefined = undefined;

    constructor(props: Props) {
        makeAutoObservable(this);
        //TODO: Fetch data of items and deliveryStatus based on the given orderId
        console.log(props.orderId);
    }
}

It requires performing the makeAutoObservable(this) to make the current object observable.
We also passed an orderId to this contructor for fetch data via API calls. Which is useful for initializing data.

So, Let's make a slight adjustment to the code snippet when declaring it.

const OrderPage = () => {
    // Passed an orderId number when create new OrderState
    const [state] = useState(() => new OrderState({ orderId: "123" }));
    return (
        <OrderPageProvider state={state}>
            <div>
                <ListItems />
                <DeliveryMilestone />
            </div>
        </OrderPageProvider>
    );
};

Retreive Value

I'll introduce two methods for accessing data in the object.
Firstly, you can directly access the object's attribute, as shown in the provided code below:

const DeliveryMilestone = observer(() => {
    const state = useOrderState();
    // Access directly to the object's attributes
    console.log("List Items: ", state.items);

    return <></>;
});

Secondly, In more complex scenarios, you can define a method to retrieve a computed value:

// Declare this method within the OrderState class followed by the `get` keyword
get isEmpty(): boolean {
    return this.items.length === 0;
}
const DeliveryMilestone = observer(() => {
    const state = useOrderState();
    // Access directly to the method, we don't need parentheses here =))
    console.log("List Items: ", state.isEmpty);

    return <></>;
});

Update Value

Sometimes, we need to update our state, for example when a button is clicked, when some input changes, a websocket message arrives, etc. In this section I'll provide two basic MobX's annotations that commonly used for modifying data.

1. Action Annotation:
This approach should only be applied to functions that intended to modify state. Hence, functions that derive information (performing lookups or filtering data) should not be marked as this annotations.

@action
setItems(data: OrderItem[]) {
    this.items = data;
}
state.setItems(data);

2. Flow Annotation:
This is an optional alternative to async / await, that make easier work. The flow mechanism will then make sure the generator either continues or throws when a yielded promise resolves. And yes, you can mark this annotation on the functions that derive information.

@flow
async fetchData(orderId: string) {
    const data = await OrderApi.getItems(orderId);
    this.setItems(data);
}

So comeback the constructor code and update it:

constructor(props: Props) {
    makeAutoObservable(this);
    //TODO: Fetch data of items and deliveryStatus based on the given orderId
    this.fetchData(props.orderId);
}

To sum up, In the first series of MobX, this article serves as a basic setup for integrating MobX with React. Leveraging the combination of MobX with helpful React hooks like createContext() and useContext() to fully utilize MobX's power.

All information about MobX is clearly and available on their main page


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí