From b28b8b29138793279a2a67e337683ed101298e71 Mon Sep 17 00:00:00 2001 From: Ivan V <390700+ivandotv@users.noreply.github.com> Date: Sat, 21 Nov 2020 00:55:09 +0100 Subject: [PATCH] add example with mobx version 6 and mobx react lite (#17493) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mobx version 6 has been released, and it's a big one.

Just released #mobx 6!

👉 makeAutoObservable 😍
👉 Decorator free by default
👉 Fully revamped docs for modern React
👉 Supersedes both MobX 4 and 5
👉 Codemod for migrationhttps://t.co/U6EpZaNhyz

— Michel Weststrate (@mweststrate) September 30, 2020
Decorator support is officially dropped, so the syntax for creating observable objects has changed (checkout store.js). There is no need for custom babel configuration anymore. In comparison to current mobx examples, the difference is that I'm using regular `React.useContext` and `React.createContext` to consume the mobx store, [this is recommended by the official documentation.](https://mobx.js.org/react-integration.html#using-external-state-in-observer-components) When the component is wrapped in the observer function, the component function is given a name so it appears correctly in the react development tools. As of mobx v6 `mobx-react` package bundles `mobx-react-lite` so I could have used that package, but I've decided to use the `lite` one, because of the size. --- examples/with-mobx-react-lite/.babelrc | 3 +- examples/with-mobx-react-lite/README.md | 72 +++--------------- .../with-mobx-react-lite/components/Clock.js | 19 ++--- .../with-mobx-react-lite/components/Page.js | 32 ++++---- .../components/StoreProvider.js | 36 +++++++++ examples/with-mobx-react-lite/package.json | 10 +-- examples/with-mobx-react-lite/pages/_app.js | 8 +- examples/with-mobx-react-lite/pages/ssg.js | 11 +++ examples/with-mobx-react-lite/pages/ssr.js | 12 +++ examples/with-mobx-react-lite/store.js | 75 ++++++++++--------- 10 files changed, 147 insertions(+), 131 deletions(-) create mode 100644 examples/with-mobx-react-lite/components/StoreProvider.js create mode 100644 examples/with-mobx-react-lite/pages/ssg.js create mode 100644 examples/with-mobx-react-lite/pages/ssr.js diff --git a/examples/with-mobx-react-lite/.babelrc b/examples/with-mobx-react-lite/.babelrc index 1ff94f7ed28e1..297803ec0d746 100644 --- a/examples/with-mobx-react-lite/.babelrc +++ b/examples/with-mobx-react-lite/.babelrc @@ -1,3 +1,4 @@ { - "presets": ["next/babel"] + "presets": ["next/babel"], + "plugins": [["@babel/plugin-proposal-class-properties", { "loose": false }]] } diff --git a/examples/with-mobx-react-lite/README.md b/examples/with-mobx-react-lite/README.md index de1a79b648e7a..2e6fa7279d64c 100644 --- a/examples/with-mobx-react-lite/README.md +++ b/examples/with-mobx-react-lite/README.md @@ -1,12 +1,20 @@ -# MobX example +# MobX V6 with Mobx React Lite -Usually splitting your app state into `pages` feels natural but sometimes you'll want to have global state for your app. This is an example on how you can use MobX that also works with our universal rendering approach. This is just a way you can do it but it's not the only one. +Usually splitting your app state into `pages` feels natural but sometimes you'll want to have global state for your app. This is an example on how you can use mobx that also works with our universal rendering approach. In this example we are going to display a digital clock that updates every second. The first render is happening in the server and then the browser will take over. To illustrate this, the server rendered clock will have a different background color than the client one. -![](http://i.imgur.com/JCxtWSj.gif) +To illustrate SSG and SSR, go to `/ssg` and `/ssr`, those pages are using Next.js data fetching methods to get the date in the server and return it as props to the page, and then the browser will hydrate the store and continue updating the date. -This example is a mobx-react-lite port of the [with-mobx](https://github.com/vercel/next.js/tree/master/examples/with-mobx) example. MobX support has been implemented using React Hooks. +The trick here for supporting universal mobx is to separate the cases for the client and the server. When we are on the server we want to create a new store every time, otherwise different users data will be mixed up. If we are in the client we want to use always the same store. That's what we accomplish on `store.js`. + +Page.js component is using the clock store to start and stop the store clock. + +Clock.js component is using the clock store to read the time. + +StoreProvider.js component is used to instantiate the `Store` both on the server and on the client. + +Both components are using a custom hook `useStore` to pull in the `Store` from the provider. ## Deploy your own @@ -25,59 +33,3 @@ yarn create next-app --example with-mobx-react-lite with-mobx-react-lite-app ``` Deploy it to the cloud with [Vercel](https://vercel.com/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). - -## Implementation details - -The initial store data is returned from the `initializeData` function that recycles existing store data if it already exists. - -```jsx -function initializeData(initialData = store || {}) { - const { lastUpdate = Date.now(), light } = initialData - return { - lastUpdate, - light: Boolean(light), - } -} -``` - -The observable store is created in a function component by passing a plain JavaScript object to the `useObservable` hook. Actions on the observable store (`start` and `stop`) are created in the same scope as the `store` in `store.js` and exported as named exports. - -```js -store = useObservable(initializeData(props.initialData)) - -start = useCallback( - action(() => { - // Async operation that mutates the store - }) -) - -stop = () => { - // Does not mutate the store -} -``` - -The component creates and exports a new React context provider that will make the store accessible to all of its descendents. - -```jsx -return {children} -``` - -The store is accessible at any depth by using the `StoreContext`. - -```js -const store = useContext(StoreContext) -``` - -The clock, under `components/Clock.js`, reacts to changes in the observable `store` by means of the `useObserver` hook. - -```jsx -return ( -
- // ... - {useObserver(() => ( - - ))} - // ... -
-) -``` diff --git a/examples/with-mobx-react-lite/components/Clock.js b/examples/with-mobx-react-lite/components/Clock.js index 9f0799e7cc975..18a2dd149fe90 100644 --- a/examples/with-mobx-react-lite/components/Clock.js +++ b/examples/with-mobx-react-lite/components/Clock.js @@ -1,7 +1,13 @@ -function Clock(props) { +import { observer } from 'mobx-react-lite' +import { useStore } from './StoreProvider' + +const Clock = observer(function Clock(props) { + // use store from the store context + const store = useStore() + return ( -
- {format(new Date(props.lastUpdate))} +
+ {store.timeString}
) -} - -const format = (t) => - `${pad(t.getUTCHours())}:${pad(t.getUTCMinutes())}:${pad(t.getUTCSeconds())}` - -const pad = (n) => (n < 10 ? `0${n}` : n) +}) export default Clock diff --git a/examples/with-mobx-react-lite/components/Page.js b/examples/with-mobx-react-lite/components/Page.js index d1877be05bee1..183a3e96d261d 100644 --- a/examples/with-mobx-react-lite/components/Page.js +++ b/examples/with-mobx-react-lite/components/Page.js @@ -1,30 +1,34 @@ -import { useObserver } from 'mobx-react-lite' +import { observer } from 'mobx-react-lite' import Link from 'next/link' -import { useContext, useEffect } from 'react' -import { StoreContext, start, stop } from '../store' +import { useEffect } from 'react' import Clock from './Clock' +import { useStore } from './StoreProvider' -function Page({ linkTo, title }) { - const store = useContext(StoreContext) +const Page = observer(function Page(props) { + // use store from the store context + const store = useStore() + //start the clock when the component is mounted useEffect(() => { - start() - return stop - }, []) + store.start() + + // stop the clock when the component unmounts + return () => { + store.stop() + } + }, [store]) return (
-

{title}

- {useObserver(() => ( - - ))} +

{props.title}

+
) -} +}) export default Page diff --git a/examples/with-mobx-react-lite/components/StoreProvider.js b/examples/with-mobx-react-lite/components/StoreProvider.js new file mode 100644 index 0000000000000..9f8cdd1be4924 --- /dev/null +++ b/examples/with-mobx-react-lite/components/StoreProvider.js @@ -0,0 +1,36 @@ +import { createContext, useContext } from 'react' +import { Store } from '../store' + +let store +export const StoreContext = createContext() + +export function useStore() { + const context = useContext(StoreContext) + if (context === undefined) { + throw new Error('useStore must be used within StoreProvider') + } + + return context +} + +export function StoreProvider({ children, initialState: initialData }) { + const store = initializeStore(initialData) + + return {children} +} + +function initializeStore(initialData = null) { + const _store = store ?? new Store() + + // If your page has Next.js data fetching methods that use a Mobx store, it will + // get hydrated here, check `pages/ssg.js` and `pages/ssr.js` for more details + if (initialData) { + _store.hydrate(initialData) + } + // For SSG and SSR always create a new store + if (typeof window === 'undefined') return _store + // Create the store once in the client + if (!store) store = _store + + return _store +} diff --git a/examples/with-mobx-react-lite/package.json b/examples/with-mobx-react-lite/package.json index fef9ae7f0905a..ed3e0f22dc889 100644 --- a/examples/with-mobx-react-lite/package.json +++ b/examples/with-mobx-react-lite/package.json @@ -2,16 +2,16 @@ "name": "with-mobx-react-lite", "version": "1.0.0", "scripts": { - "dev": "next", + "dev": "next dev", "build": "next build", "start": "next start" }, "dependencies": { - "mobx": "^5.9.0", - "mobx-react-lite": "^1.1.1", + "mobx": "^6.0.3", + "mobx-react-lite": "^3.1.5", "next": "latest", - "react": "^16.8.4", - "react-dom": "^16.8.4" + "react": "^17.0.1", + "react-dom": "^17.0.1" }, "license": "MIT" } diff --git a/examples/with-mobx-react-lite/pages/_app.js b/examples/with-mobx-react-lite/pages/_app.js index 00fc35b660d92..7aff31ee3c05c 100644 --- a/examples/with-mobx-react-lite/pages/_app.js +++ b/examples/with-mobx-react-lite/pages/_app.js @@ -1,11 +1,9 @@ -import { InjectStoreContext } from '../store' +import { StoreProvider } from '../components/StoreProvider' export default function App({ Component, pageProps }) { - // If your page has Next.js data fetching methods returning a state for the Mobx store, - // then you can hydrate it here. return ( - + - + ) } diff --git a/examples/with-mobx-react-lite/pages/ssg.js b/examples/with-mobx-react-lite/pages/ssg.js new file mode 100644 index 0000000000000..496dac3e7063f --- /dev/null +++ b/examples/with-mobx-react-lite/pages/ssg.js @@ -0,0 +1,11 @@ +import Page from '../components/Page' + +export default function SSG() { + return +} + +// If you build and start the app, the date returned here will have the same +// value for all requests, as this method gets executed at build time. +export function getStaticProps() { + return { props: { initialState: { lastUpdate: Date.now() } } } +} diff --git a/examples/with-mobx-react-lite/pages/ssr.js b/examples/with-mobx-react-lite/pages/ssr.js new file mode 100644 index 0000000000000..f414935aa0b25 --- /dev/null +++ b/examples/with-mobx-react-lite/pages/ssr.js @@ -0,0 +1,12 @@ +import Page from '../components/Page' + +export default function SSR() { + return +} + +// The date returned here will be different for every request that hits the page, +// that is because the page becomes a serverless function instead of being statically +// exported when you use `getServerSideProps` or `getInitialProps` +export function getServerSideProps() { + return { props: { initialState: { lastUpdate: Date.now() } } } +} diff --git a/examples/with-mobx-react-lite/store.js b/examples/with-mobx-react-lite/store.js index d185ec941bf13..fa2ad1d2737b9 100644 --- a/examples/with-mobx-react-lite/store.js +++ b/examples/with-mobx-react-lite/store.js @@ -1,44 +1,45 @@ -import { action } from 'mobx' -import { useObservable, useStaticRendering } from 'mobx-react-lite' -import { createContext, useCallback } from 'react' - -const isServer = typeof window === 'undefined' -// eslint-disable-next-line react-hooks/rules-of-hooks -useStaticRendering(isServer) - -let StoreContext = createContext() -let start -let stop -let store - -function initializeData(initialData = store || {}) { - const { lastUpdate = Date.now(), light } = initialData - return { - lastUpdate, - light: Boolean(light), - } -} +import { action, observable, computed, runInAction, makeObservable } from 'mobx' +import { enableStaticRendering } from 'mobx-react-lite' + +enableStaticRendering(typeof window === 'undefined') -function InjectStoreContext({ children, initialData }) { - let timerInterval = null - store = useObservable(initializeData(initialData)) +export class Store { + lastUpdate = 0 + light = false - start = useCallback( - action(() => { - timerInterval = setInterval(() => { - store.lastUpdate = Date.now() - store.light = true - }, 1000) + constructor() { + makeObservable(this, { + lastUpdate: observable, + light: observable, + start: action, + timeString: computed, }) - ) + } - stop = () => { - if (timerInterval) { - clearInterval(timerInterval) - } + start = () => { + this.timer = setInterval(() => { + runInAction(() => { + this.lastUpdate = Date.now() + this.light = true + }) + }, 1000) } - return {children} -} + get timeString() { + const pad = (n) => (n < 10 ? `0${n}` : n) + const format = (t) => + `${pad(t.getUTCHours())}:${pad(t.getUTCMinutes())}:${pad( + t.getUTCSeconds() + )}` + return format(new Date(this.lastUpdate)) + } + + stop = () => clearInterval(this.timer) + + hydrate = (data) => { + if (!data) return -export { InjectStoreContext, StoreContext, initializeData, start, stop, store } + this.lastUpdate = data.lastUpdate !== null ? data.lastUpdate : Date.now() + this.light = !!data.light + } +}