83
How to use Mobx in Next.js application (with demo)
Hello, in these article i will create next.js application for showing books with mobx. I will provide:
- SSR with mobx hydration
- Using hydrated mobx on client side
- Show possible mistakes
English is not my native language, but i hope the code will tell you more than text :D
If you don't want to read welcome to sandbox :D - Demo
Before you start, you need to understand exactly what order the server and client rendering takes place in next.js.
Here are some nuances about the order in which next.js works and in which order we expect hydration.
-
Component
- page component (pages/index.jsx) -
Next-server
- next.js application host -
_app
- next.js app component (pages/_app.jsx) -
API-server
- backend-application (not provided in demo, but in real world will be)
First, the server side props are called, then the component's _app is parsing, and only then the HTML page is rendered. Server pre-rendering has occurred. The client receives the statics and begins to deploy the react environment, starting with the _app of the application and ending with the component
You can check the order of execution in the demo, there is the logic for logging these steps to the console
The mobx state hydration scheme in next.js application is presented below
The structure of the project will be as follows:
|components
- Folder with all app components (exclude pages)
| |-BoookCard
|pages
(every folder is separate app route and page. Also the service components (_app, _document) are stored here)
| |-_app
| |-index
(main page)
|store
(mobx store)
| |-Books
(BooksStore)
| |-index
(useStore hook and initialize store methods)
|utils
(mock datas and other utils)
| |-index
Let's init BooksStore (description in comments)
import { makeAutoObservable } from "mobx";
import { books, clientBooks } from "../utils";
class BooksStore {
constructor() {
// define and init observables
this.books = [];
this.searchParam = "";
(make all object properties observables, getters computed, methods actions)
makeAutoObservable(this);
}
setSearchParam = (param) => {
this.searchParam = param;
};
setBooks = (books) => (this.books = books);
get filteredBooks() {
return this.books.filter((book) =>
book.title.toLowerCase().includes(this.searchParam.toLowerCase())
);
}
get totalBooks() {
return this.books.length;
}
// if data is provided set this data to BooksStore
hydrate = (data) => {
if (!data) return;
this.setBooks(data.books);
};
// special method for demonstration
fetchAndSetBooksOnClient = async () => {
const newBooks = await Promise.resolve([...books, ...clientBooks]);
console.log(newBooks);
this.setBooks(newBooks);
};
}
export default BooksStore
Note that on the client side we receive also Harry Potter books in fetch method. This is done to show the state of the store on the server side and on the client side.
We need to create a new store on every server request, and use one store on client side. Otherwise you will have problems with store
In next step we will provide store/index.js
file:
// we need to enable static rendering for prevent rerender on server side and leaking memory
import { enableStaticRendering } from "mobx-react-lite";
import BooksStore from '../BooksStore'
// enable static rendering ONLY on server
enableStaticRendering(typeof window === "untdefined")
// init a client store that we will send to client (one store for client)
let clientStore
const initStore = (initData) => {
// check if we already declare store (client Store), otherwise create one
const store = clientStore ?? new BooksStore();
// hydrate to store if receive initial data
if (initData) store.hydrate(initData)
// Create a store on every server request
if (typeof window === "undefined") return store
// Otherwise it's client, remember this store and return
if (!clientStore) clientStore = store;
return store
}
// Hoook for using store
export function useStore(initData) {
return initStore(initData)
}
We need to create and provide _app component in pages directory. Let's do it
import { useStore } from "../store";
import { createContext } from "react";
import { getSide } from "../utils";
export const MobxContext = createContext();
const MyApp = (props) => {
console.log("hello from _app - ", getSide());
const { Component, pageProps, err } = props;
const store = useStore(pageProps.initialState);
return (
<MobxContext.Provider value={store}>
<Component {...pageProps} err={err} />
</MobxContext.Provider>
);
};
export default MyApp;
import { getSide, books } from "../utils";
import { useContext } from "react";
import { MobxContext } from "./_app";
import BookCard from "../components/BookCard";
import { observer } from "mobx-react-lite";
const IndexPage = () => {
const {
totalBooks,
filteredBooks,
setSearchParam,
fetchAndSetBooksOnClient
} = useContext(MobxContext);
console.log("hello from Page component ", getSide());
const handleOnInputChange = (e) => {
setSearchParam(e.target.value);
};
return (
<div>
<h1>Books:</h1>
<h3>TotalBooks: {totalBooks}</h3>
<button onClick={fetchAndSetBooksOnClient}>Fetch on Client</button>
<input placeholder="search" type="text" onChange={handleOnInputChange} />
<hr />
<div style={{ display: "flex" }}>
{filteredBooks.map((book, index) => (
<BookCard key={index} book={book} />
))}
</div>
</div>
);
};
export const getServerSideProps = async () => {
console.log("making server request before app", getSide());
// here could be any async request for fetching data
// const books = BooksAgent.getAll();
return {
props: {
initialState: {
booksStore: {
books
}
}
}
};
};
export default observer(IndexPage);
And that's all. You can check mobx reactivity by adding new book (fetch on Client button) and searching book by title.
- Not create a new store on every server request (store/index:12) - on each request, the storage will be refilled with data
- Forgot to do hydration (store/index:10) - non-compliance with content on server and client
- Not using one store on client and server (MobxContext and useStore ONLY in _app component) - non-compliance with content on server and client
I'm not the first to implement or explain this solution, I just tried to describe in more detail how it works and make an example more realistic
Offical github example
Another cool article
83