未验证 提交 b28b8b29 编写于 作者: I Ivan V 提交者: GitHub

add example with mobx version 6 and mobx react lite (#17493)

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.
上级 e1737809
{
"presets": ["next/babel"]
"presets": ["next/babel"],
"plugins": [["@babel/plugin-proposal-class-properties", { "loose": false }]]
}
# 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 <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>
)
```
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;
......@@ -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
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
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
}
......@@ -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"
}
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>
)
}
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() } } }
}
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() } } }
}
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
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册