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
-
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
-
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
-
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
-
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:
-
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
-
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
-
New utils/API. Js
import { observableStore } from '@/utils/store'; export default { getLang() { return observableStore.rootStore.lang } } Copy the code
-
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:
-
The server rootState variable value is saved and is not initialized after refreshing the page.
-
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
-
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
-
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
-
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
-
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
-
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.