Skip to content

Commit

Permalink
add example with mobx version 6 and mobx react lite (#17493)
Browse files Browse the repository at this point in the history
Mobx version 6 has been released, and it's a big one.

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="en" dir="ltr">Just released <a href="https://twitter.com/hashtag/mobx?src=hash&amp;ref_src=twsrc%5Etfw">#mobx</a> 6! <br><br>👉 makeAutoObservable 😍<br>👉 Decorator free by default<br>👉 Fully revamped docs for modern React <br>👉 Supersedes both MobX 4 and 5<br>👉 Codemod for migration<a href="https://t.co/U6EpZaNhyz">https://t.co/U6EpZaNhyz</a></p>&mdash; Michel Weststrate (@mweststrate) <a href="https://twitter.com/mweststrate/status/1311344102991159296?ref_src=twsrc%5Etfw">September 30, 2020</a></blockquote>


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.
  • Loading branch information
ivandotv authored Nov 20, 2020
1 parent e173780 commit b28b8b2
Show file tree
Hide file tree
Showing 10 changed files with 147 additions and 131 deletions.
3 changes: 2 additions & 1 deletion examples/with-mobx-react-lite/.babelrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"presets": ["next/babel"]
"presets": ["next/babel"],
"plugins": [["@babel/plugin-proposal-class-properties", { "loose": false }]]
}
72 changes: 12 additions & 60 deletions examples/with-mobx-react-lite/README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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 <StoreContext.Provider value={store}>{children}</StoreContext.Provider>
```

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 (
<div>
// ...
{useObserver(() => (
<Clock lastUpdate={store.lastUpdate} light={store.light} />
))}
// ...
</div>
)
```
19 changes: 10 additions & 9 deletions examples/with-mobx-react-lite/components/Clock.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className={props.light ? 'light' : ''}>
{format(new Date(props.lastUpdate))}
<div className={store.light ? 'light' : ''}>
{store.timeString}
<style jsx>{`
div {
padding: 15px;
Expand All @@ -17,11 +23,6 @@ function Clock(props) {
`}</style>
</div>
)
}

const format = (t) =>
`${pad(t.getUTCHours())}:${pad(t.getUTCMinutes())}:${pad(t.getUTCSeconds())}`

const pad = (n) => (n < 10 ? `0${n}` : n)
})

export default Clock
32 changes: 18 additions & 14 deletions examples/with-mobx-react-lite/components/Page.js
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<h1>{title}</h1>
{useObserver(() => (
<Clock lastUpdate={store.lastUpdate} light={store.light} />
))}
<h1>{props.title}</h1>
<Clock />
<nav>
<Link href={linkTo}>
<Link href={props.linkTo}>
<a>Navigate</a>
</Link>
</nav>
</div>
)
}
})

export default Page
36 changes: 36 additions & 0 deletions examples/with-mobx-react-lite/components/StoreProvider.js
Original file line number Diff line number Diff line change
@@ -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 <StoreContext.Provider value={store}>{children}</StoreContext.Provider>
}

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
}
10 changes: 5 additions & 5 deletions examples/with-mobx-react-lite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
8 changes: 3 additions & 5 deletions examples/with-mobx-react-lite/pages/_app.js
Original file line number Diff line number Diff line change
@@ -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 (
<InjectStoreContext initialData={pageProps.initialStoreData}>
<StoreProvider {...pageProps}>
<Component {...pageProps} />
</InjectStoreContext>
</StoreProvider>
)
}
11 changes: 11 additions & 0 deletions examples/with-mobx-react-lite/pages/ssg.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Page from '../components/Page'

export default function SSG() {
return <Page title="Index Page" linkTo="/other" />
}

// 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() } } }
}
12 changes: 12 additions & 0 deletions examples/with-mobx-react-lite/pages/ssr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Page from '../components/Page'

export default function SSR() {
return <Page title="Index Page" linkTo="/other" />
}

// 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() } } }
}
75 changes: 38 additions & 37 deletions examples/with-mobx-react-lite/store.js
Original file line number Diff line number Diff line change
@@ -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 <StoreContext.Provider value={store}>{children}</StoreContext.Provider>
}
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
}
}

0 comments on commit b28b8b2

Please sign in to comment.