Skip to content

Commit

Permalink
Refactor useHydratedStore hook. Remove mst-persist (#2863)
Browse files Browse the repository at this point in the history
- consolidate `useStore` and `useHydratedStore`, so that `useHydratedStore` calls `useStore` to create the new store.
- add a `persist` helper to manage store snapshots.
- remove `mst-persist`.

Move Classifier hooks to @hooks
Load hooks from `/src/hooks`, aliased to `@hooks`.

Test the useHydratedStore hook

Consolidate store hooks
Replace the store hooks with a single `useHydratedStore` hook. Use synchronous storage.
  • Loading branch information
eatyourgreens authored Mar 22, 2022
1 parent 795b174 commit cbc7607
Show file tree
Hide file tree
Showing 16 changed files with 173 additions and 125 deletions.
1 change: 0 additions & 1 deletion packages/lib-classifier/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
"i18next": "~21.6.3",
"lodash": "~4.17.11",
"mobx-utils": "~6.0.4",
"mst-persist": "~0.1.3",
"react-i18next": "~11.15.1",
"react-player": "~2.9.0",
"react-resize-detector": "~7.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
tutorials as tutorialsClient
} from '@zooniverse/panoptes-js'

import { useHydratedStore, useStore, useWorkflowSnapshot } from './hooks'
import { useHydratedStore, useWorkflowSnapshot } from '@hooks'
import { unregisterWorkers } from '../../workers'
import Classifier from './Classifier'

Expand Down Expand Up @@ -62,15 +62,12 @@ export default function ClassifierContainer({
workflowID
}) {

const classifierStore = useStore({
authClient,
client,
initialState: {}
})
const storeEnvironment = { authClient, client }

const workflowSnapshot = useWorkflowSnapshot(workflowID)

const loaded = useHydratedStore(classifierStore, cachePanoptesData, `fem-classifier-${project.id}`)
const classifierStore = useHydratedStore(storeEnvironment, cachePanoptesData, `fem-classifier-${project.id}`)
const loaded = !!classifierStore

useEffect(function onMount() {
console.log('resetting stale user data')
Expand Down
26 changes: 0 additions & 26 deletions packages/lib-classifier/src/components/Classifier/hooks/Readme.md

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

1 change: 0 additions & 1 deletion packages/lib-classifier/src/helpers/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as featureDetection from './featureDetection'
export { default as asyncSessionStorage } from './asyncSessionStorage'
export { default as createLocationCounts } from './createLocationCounts'
export { featureDetection }
export { default as findLocationsByMediaType } from './findLocationsByMediaType'
Expand Down
19 changes: 19 additions & 0 deletions packages/lib-classifier/src/hooks/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Classifier hooks

## useHydratedStore

Create a `mobx-state-tree` store from an optional stored [snapshot](https://mobx-state-tree.js.org/concepts/snapshots). Adds an `onSnapshot` handler to keep the stored snapshot updated when the store changes.

Returns the new store when hydration is complete. Snapshots are stored in session storage, so that they don't persist across tabs or windows.

```js
const classifierStore = useHydratedStore({ authClient, client }, cachePanoptesData = false, storageKey)
```

## useWorkflowSnapshot

A wrapper for [`useSWR`](https://swr.vercel.app/), which fetches a workflow by ID, using the default SWR options. The workflow will refresh on visibility change (eg. waking from sleep), or when the classifier receives focus.

```js
const workflowSnapshot = useWorkflowSnapshot(workflowID)
```
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export { default as useHydratedStore } from './useHydratedStore'
export { default as useStore } from './useStore'
export { default as useWorkflowSnapshot } from './useWorkflowSnapshot'
50 changes: 50 additions & 0 deletions packages/lib-classifier/src/hooks/useHydratedStore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import makeInspectable from 'mobx-devtools-mst'
import { addDisposer, destroy, onSnapshot } from 'mobx-state-tree'
import { useMemo } from 'react'

import RootStore from '@store'

let store = null
const storage = window.sessionStorage

function loadSnapshot(storageKey) {
const data = storage.getItem(storageKey)
return JSON.parse(data) || {}
}

function persist(storageKey, _store) {
function _saveSnapshot(snapshot) {
const data = JSON.stringify(snapshot)
storage.setItem(storageKey, data)
}
const snapshotDisposer = onSnapshot(_store, _saveSnapshot)
return addDisposer(_store, snapshotDisposer)
}

function initStore({ cachePanoptesData, storageKey, storeEnv }) {
if (store === null) {
let initialState = {}

if (cachePanoptesData) {
initialState = loadSnapshot(storageKey)
}

store = RootStore.create(initialState, storeEnv)

if (cachePanoptesData) {
persist(storageKey, store)
}
makeInspectable(store)
}
return store
}

export function cleanStore() {
destroy(store)
store = null
}

export default function useHydratedStore(storeEnv = {}, cachePanoptesData = false, storageKey) {
const _store = useMemo(() => initStore({ cachePanoptesData, storageKey, storeEnv }), [cachePanoptesData, storageKey, storeEnv])
return _store
}
98 changes: 98 additions & 0 deletions packages/lib-classifier/src/hooks/useHydratedStore.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { applySnapshot, getSnapshot } from 'mobx-state-tree'
import React from 'react'
import { renderHook } from '@testing-library/react-hooks/pure'

import mockStore from '@test/mockStore'
import RootStore from '@store/RootStore'
import { cleanStore } from './useHydratedStore'
import { useHydratedStore } from '.'

describe('Hooks > useHydratedStore', function () {
describe('without an existing store', function () {
let store

beforeEach(function () {
const { authClient, client } = mockStore()
const { result } = renderHook(() => useHydratedStore({ authClient, client }, false, 'test-key'))
store = result.current
})

afterEach(function () {
cleanStore()
})

it('should create a new store', function () {
const mockStore = getSnapshot(RootStore.create({}))
const snapshot = getSnapshot(store)
expect(snapshot).to.deep.equal(mockStore)
})
})

describe('with an existing store', function () {
let store
let newStore

beforeEach(function () {
const { authClient, client } = mockStore()
const { result: firstRun } = renderHook(() => useHydratedStore({ authClient, client }, false, 'test-key'))
store = firstRun.current
const { result: secondRun } = renderHook(() => useHydratedStore({ authClient, client }, false, 'test-key'))
newStore = secondRun.current
})

afterEach(function () {
cleanStore()
})

it('should use the existing store', function () {
expect(newStore).to.deep.equal(store)
})
})

describe('with session storage enabled', function () {
let store

beforeEach(function () {
const expectedStore = mockStore()
const { authClient, client } = expectedStore
const { result } = renderHook(() => useHydratedStore({ authClient, client }, true, 'test-key'))
store = result.current
const mockSnapshot = getSnapshot(expectedStore)
applySnapshot(store.projects, mockSnapshot.projects)
})

afterEach(function () {
cleanStore()
})

it('should save snapshots in session storage', function () {
const mockSnapshot = window.sessionStorage.getItem('test-key')
const snapshot = getSnapshot(store)
expect(JSON.stringify(snapshot)).to.equal(mockSnapshot)
})
})

describe('with a saved snapshot', function () {
let store
let mockSnapshot

beforeEach(function () {
const expectedStore = mockStore()
mockSnapshot = getSnapshot(expectedStore)
window.sessionStorage.setItem('test-key', JSON.stringify(mockSnapshot))

const { authClient, client } = expectedStore
const { result } = renderHook(() => useHydratedStore({ authClient, client }, true, 'test-key'))
store = result.current
})

afterEach(function () {
cleanStore()
})

it('should load the snapshot into the store', function () {
const snapshot = getSnapshot(store)
expect(snapshot.projects).to.deep.equal(mockSnapshot.projects)
})
})
})
1 change: 1 addition & 0 deletions packages/lib-classifier/webpack.dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ module.exports = {
resolve: {
alias: {
'@components': path.resolve(__dirname, 'src/components'),
'@hooks': path.resolve(__dirname, 'src/hooks'),
'@helpers': path.resolve(__dirname, 'src/helpers'),
'@plugins': path.resolve(__dirname, 'src/plugins'),
'@store': path.resolve(__dirname, 'src/store'),
Expand Down
1 change: 1 addition & 0 deletions packages/lib-classifier/webpack.dist.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ module.exports = {
resolve: {
alias: {
'@components': path.resolve(__dirname, 'src/components'),
'@hooks': path.resolve(__dirname, 'src/hooks'),
'@helpers': path.resolve(__dirname, 'src/helpers'),
'@plugins': path.resolve(__dirname, 'src/plugins'),
'@store': path.resolve(__dirname, 'src/store'),
Expand Down
5 changes: 0 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -12044,11 +12044,6 @@ mst-middlewares@~5.1.0:
resolved "https://registry.yarnpkg.com/mst-middlewares/-/mst-middlewares-5.1.3.tgz#059a2cf4e44cdb44ae92eb4a4e8ce47f6c117c66"
integrity sha512-zVPxLO6kQFMTU0S7xYoyiSml8nUP5Ekto3CY4Ra7cGwbuLOwIDBgCyNIEEtLpq95KFbhAEl9oIchg4RW13iCZg==

mst-persist@~0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/mst-persist/-/mst-persist-0.1.3.tgz#ac7203f3ab36085106c4293d4a9cd67dd5613708"
integrity sha512-G2IzmTvi2E8zOTTCiMENkli+KmANhpPNlkguOEJp0dioqfdETDQjLO6QKy5hXhDTRIQhu48IlDJeNdyeZ8NIaw==

multicast-dns-service-types@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901"
Expand Down

0 comments on commit cbc7607

Please sign in to comment.