The use of mobx in next.js involves three plug-ins:

  • mobx
  • mobx-react
  • mobx-react-lite

If you are not familiar with Mobx, it is recommended to read the documentation getting Started with Mobx.

If you are not familiar with React Context, you are advised to read the document React Context

Features that need to be implemented with Mobx in your project

There are global stores, page stores, and component stores:

  • Data such as language and currency are stored in the global store, and all JS codes (including non-component JS plug-ins) are shared.

  • Page Store is a single page’s own store module, only for the use of the page itself;

  • Component store refers to store modules used by components, such as components shared by multiple pages or components with multiple levels of separation.

Access global variables in all components

Context is required for cross-component transmission, and all components need to read the Context variable, so create a new file to hold the variable, which is easy for components to import

  1. Create a new utils/store.ts file and create the storeContext variable

    import { createContext, useContext } from 'react';
    
    export const storeContext = createContext(Object.create(null));
    Copy the code
  2. Inject variables that need to be shared through the Provider in the outermost component (Pages /_app.tsx)

    import { storeContext } from '@/utils/store'; import { useLocalStore } from 'mobx-react-lite'; import App from 'next/app'; function MyApp(props) { const { Component, pageProps, lang } = props; const store = useLocalStore(() => ({ rootStore: { lang } })); return ( <storeContext.Provider value={store}> <Component {... pageProps} /> </storeContext.Provider> ) } MyApp.getInitialProps = async (appContext) => { const appProps = await App.getInitialProps(appContext); / / the value of the server for lang, passed through the props MyApp component const lang = appContext. CTX. The req. Query. LNG; return { ... appProps, lang } }; export default MyApp;Copy the code
  3. The page component reads variables through the useContext hook

    import { storeContext } from '@/utils/store';
    
    function Page() {
        const store = React.useContext(storeContext);
        return <div>Current Lang: {store.rootStore.lang}</div>
    }
    
    export default Page;
    Copy the code
  4. Encapsulate the React. UseContext method and add the useStore interface to utils/store.ts

    import { useContext } from 'react';
    
    export const useStore = (a)= > {
        const store = useContext(storeContext);
        if(! store) {// this is especially useful in TypeScript so you don't need to be checking for null all the time
            throw new Error('useStore must be used within a StoreProvider.');
        }
        return store;
    };
    Copy the code

    Use in components

    import { useStore } from '@/utils/store';
    
    function Page() {
        const { rootStore } = useStore();
        return <div>lang in store: {rootStore.lang}</div>
    }
    Copy the code

Non-component JS reads the global store

Taking the Ajax request plug-in api.js as an example, the plug-in needs to support both the server and the client to initiate ajax requests, and the URL format is HTTP (S)://api.com/en/getXXX. The current language must be written after the domain name.

In the component example above, the react. useContext used to read global variables can only be used in hooks. Api.js is not a component and cannot be read from the Context.

Therefore, sharing can only be achieved by sharing an object that can be used by both components and non-components.

In the Introduction to MobX:

UseLocalStore is an Observable wrapper. The following two methods work the same

const [person] = useState((a)= > observable({ name: 'John' }));
const person = useLocalStore((a)= > ({ name: 'John' }));
Copy the code

Try sharing an Observable to do this:

  1. Added observableStore for sharing in utils/store.ts

    import { observable } from 'mobx';
    
    // Store object shared by all components
    export const observableStore = observable({
        rootStore: { lang: 'en'}});Copy the code
  2. Modify the pages / _app TSX

    import { storeContext, observableStore } from '@/utils/store'; import { useState } from 'react'; function MyApp(props) { const { Component, pageProps, lang } = props; const [store] = useState(() => observableStore); store.rootStore.lang = lang; return ( <storeContext.Provider value={store}> <Component {... pageProps} /> </storeContext.Provider> ) }Copy the code
  3. New utils/API. Js

    import { observableStore } from '@/utils/store';
    
    export default {
        getLang() {
            return observableStore.rootStore.lang
        }
    }
    Copy the code
  4. Modifying page components

    import { useStore } from '@/utils/store';
    import api from '@/utils/api';
    
    function Page(props) {
        const {rootStore} = useStore();
        return (
            <div>
                lang in store: {rootStore.lang}<br/>
                lang in component api: {api.getLang()}<br/>
                lang in getServerSideProps api: {props.langInServer}
            </div>)}export function getServerSideProps() {
        return {
            props: {
                langInServer: api.getLang()
            }
        }
    }
    
    export default Page;
    Copy the code

There was an issue with the langInServer that getServerSideProps passed to the component:

The language in the URL is changed from en to de, which displays: lang in getServerSideProps API: en

This changes to the Lang in getServerSideProps API: de

Draw the conclusion from the phenomenon:

  1. The server rootState variable value is saved and is not initialized after refreshing the page.

  2. Both the server and client in the Page component get the updated rootStore data, but the getServerSideProps can only get the data before the update.

Initialize rootState when the page is refreshed

  1. Modify utils/store.ts file and add initStore method

    // Create a new store object
    const createStore = (rootStoreData) = > observable({
        rootStore: Object.assign({
            lang: 'en'
        }, rootStoreData)
    });
    
    // Store object shared by all components
    export let observableStore = null;
    
    // Store initialization
    export const initStore = (rootStoreData) = > {
        observableStore = createStore(rootStoreData);
    }
    Copy the code
  2. Modify the pages / _app TSX

    import { storeContext, observableStore, initStore } from '@/utils/store';
    import { useState } from 'react';
    import App from 'next/app';
    
    function MyApp(props) {
        const { Component, pageProps, lang } = props;
    
       initStore({ lang });
    
        const [store] = useState((a)= > observableStore);
    
        // return ...
    }
    
    MyApp.getInitialProps = async (appContext) => {
        const lang = appContext.ctx.req.query.lng;
        // The server initializes the store
        // Myapp.getInitialprops is executed on getServerSideProps of the page component to resolve the problem of not getting the latest value on the page component server
        initStore({ lang });
        / /...
    };
    
    export default MyApp;
    Copy the code

Store module dynamic registration

The most common way to share stores online is to put all store files in the same directory and register all modules at initialization.

The way I want to use it is to register only rootStore at initialization, dynamically registering the store module for each page or component.

Mobx provides an extendObservable method to do this.

Add the addStore method to utils/store.ts

// Dynamically add store modules
export const addStore = (moduleName, moduleData) = > {
    const store = useStore();
    if(! store[moduleName]) {const module = Object.create(null);
        module[moduleName] = moduleData;
        extendObservable(store, module);
    }
    return store;
};
Copy the code

Components in the sample

import { addStore } from '@/utils/store';
import { useObserver } from 'mobx-react-lite';
import { useEffect } from 'react';

function createStore(defaultPoints) {
    return {
        points: defaultPoints,
        get pointsTip() {
            return 'Total Points: ' + this.points;
        },
        setPoints(num) {
            this.points = num;
        },
        addPoints() {
            this.points += 1; }}}function Page(props) {
    // Dynamically add the page Store module
    const { thePage } = addStore('thePage', createStore(props.defaultPoints));

    return useObserver((a)= > (
            <div onClick={()= > thePage.addPoints()}>
                {thePage.pointsTip}
            </div>))}// The server returns the initial value
export function getServerSideProps() {
    return {
        props: {
            defaultPoints: 50}}}export default Page;
Copy the code

Further optimization of packaging

  1. Privatization of variables

    Store. ts export two objects: storeContext and observableStore

    export const StoreProvider = ({ children }) => {
        const [store] = useState(() => observableStore);
        return <storeContext.Provider value={store}>{children}</storeContext.Provider>;
    };
    Copy the code

    The _app. TSX usage mode is changed

    import { StoreProvider, initStore } from '@/utils/store'; function MyApp(props) { const { Component, pageProps, lang } = props; initStore({ lang }); return <StoreProvider><Component {... pageProps} /></StoreProvider> }Copy the code
  2. Rewrite the useStore interface to support non-component calls

    export const useStore = (a)= > {
        let store;
        try {
            store = useContext(storeContext);
        } catch (e) {
            // Non-hook components will not be able to use useContext and will return observableStore directly
            store = observableStore;
        }
        if(! store) {throw new Error('Store has not been initialized.');
        }
        return store;
    };
    Copy the code
  3. How to use api.js

    import { useStore } from '@/utils/store';
    
    export default {
        getLang() {
            const { rootStore } = useStore();
            return rootStore.lang
        }
    }
    Copy the code

At this point, you have satisfied the functionality required for web site development.