Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Examples] Fix with mobx #11907

Merged
merged 8 commits into from
Apr 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 4 additions & 18 deletions examples/with-mobx/README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,14 @@
# MobX example

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.

The clock, under `components/Clock.js`, has access to the state using the `inject` and `observer` functions from `mobx-react`. In this case Clock is a direct child from the page but it could be deep down the render tree.

This example is a mobx port of the [with-redux](https://github.com/zeit/next.js/tree/master/examples/with-redux) example. Decorator support is activated by adding a `.babelrc` file at the root of the project:
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`.

```json
{
"presets": ["next/babel"],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }]
]
}
```
The clock, under `components/Clock.js`, has access to the state using the `inject` and `observer` functions from `mobx-react`. In this case Clock is a direct child from the page but it could be deep down the render tree.

## Deploy your own

Expand Down Expand Up @@ -58,7 +48,3 @@ yarn dev
```

Deploy it to the cloud with [ZEIT Now](https://zeit.co/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).

### Rehydrating with server data

Be aware that data that was used on the server (and provided via one of Next.js data fetching methods) will be stringified in order to rehydrate the client with it. That means, if you create a store that is, say, an `ObservableMap` and give it as prop to a page, then the server will render appropriately. But stringifying it for the client will turn the `ObservableMap` to an ordinary JavaScript object (which does not have `Map`-style methods and is not an observable). So it is better to create the store as a normal object and turn it into a `Observable` in the `render()` method. This way both sides have an `Observable` to work with.
13 changes: 5 additions & 8 deletions examples/with-mobx/components/Clock.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
export default props => {
import { observer } from 'mobx-react'
const Clock = observer(props => {
return (
<div className={props.light ? 'light' : ''}>
{format(new Date(props.lastUpdate))}
{props.timeString}
<style jsx>{`
div {
padding: 15px;
Expand All @@ -17,9 +18,5 @@ export default 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
2 changes: 1 addition & 1 deletion examples/with-mobx/components/Page.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class Page extends React.Component {
<div>
<h1>{this.props.title}</h1>
<Clock
lastUpdate={this.props.store.lastUpdate}
timeString={this.props.store.timeString}
light={this.props.store.light}
/>
<nav>
Expand Down
8 changes: 4 additions & 4 deletions examples/with-mobx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
"start": "next start"
},
"dependencies": {
"mobx": "^2.7.0",
"mobx-react": "^4.0.4",
"mobx": "^5.15.4",
"mobx-react": "^6.2.2",
"next": "latest",
"react": "^16.7.0",
"react-dom": "^16.7.0"
"react": "^16.13.1",
"react-dom": "^16.13.1"
},
"license": "ISC",
"devDependencies": {
Expand Down
16 changes: 2 additions & 14 deletions examples/with-mobx/pages/_app.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,8 @@
import { useMemo, useEffect } from 'react'
import { Store } from '../store'
import { Provider } from 'mobx-react'
import { useStore } from '../store'

export default function App({ Component, pageProps }) {
const store = useMemo(() => {
return new Store()
}, [])

useEffect(() => {
// If your page has Next.js data fetching methods returning a state for the Mobx store,
// then you can hydrate it here.
const { initialState } = pageProps
if (initialState) {
store.hydrate(initialState)
}
}, [store, pageProps])
const store = useStore(pageProps.initialState)

return (
<Provider store={store}>
Expand Down
11 changes: 11 additions & 0 deletions examples/with-mobx/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/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() } } }
}
59 changes: 46 additions & 13 deletions examples/with-mobx/store.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,60 @@
import { action, observable } from 'mobx'
import { action, observable, computed, runInAction } from 'mobx'
import { useStaticRendering } from 'mobx-react'

import { useMemo } from 'react'
// eslint-disable-next-line react-hooks/rules-of-hooks
useStaticRendering(typeof window === 'undefined')

export class Store {
let store

class Store {
@observable lastUpdate = 0
@observable light = false

hydrate(serializedStore) {
this.lastUpdate =
serializedStore.lastUpdate != null
? serializedStore.lastUpdate
: Date.now()
this.light = !!serializedStore.light
}

@action start = () => {
this.timer = setInterval(() => {
this.lastUpdate = Date.now()
this.light = true
runInAction(() => {
this.lastUpdate = Date.now()
this.light = true
})
}, 1000)
}

@computed 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

this.lastUpdate = data.lastUpdate !== null ? data.lastUpdate : Date.now()
this.light = !!data.light
}
}

export 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
}

export function useStore(initialState) {
const store = useMemo(() => initializeStore(initialState), [initialState])
return store
}