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

Classifier: Refactor and test useHydratedStore hook. Remove mst-persist #2863

Merged
merged 1 commit into from
Mar 22, 2022
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
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