Skip to content

Commit

Permalink
💥 Replace custom data store with Map
Browse files Browse the repository at this point in the history
  • Loading branch information
exah committed Mar 22, 2020
1 parent 4fe9805 commit 7b05bf1
Show file tree
Hide file tree
Showing 11 changed files with 53 additions and 141 deletions.
22 changes: 21 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
import { AsyncState } from './types'

export const IS_CLIENT = typeof window !== 'undefined'
export const IS_SERVER = !IS_CLIENT
export const INITIAL_ID = -1

export const enum ActionTypes {
START = 'START',
FINISH = 'FINISH',
}

export const INITIAL_STATE: AsyncState<null> = {
isReady: false,
isLoading: false,
error: null,
result: null,
}

export const FINISH_STATE: AsyncState<null> = {
isReady: true,
isLoading: false,
error: null,
result: null,
}
4 changes: 2 additions & 2 deletions src/context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createContext } from 'react'
import { defaultDataStore } from './data-store'
import { defaultStore } from './data-store'

/**
* Provides `dataStore` created with {@link createDataStore} to {@link withData} components using [React Context](http://reactjs.org/docs/context.html).
Expand All @@ -23,7 +23,7 @@ import { defaultDataStore } from './data-store'
* )
*/

export const DataContext = createContext(defaultDataStore)
export const DataContext = createContext(defaultStore)

export const DataProvider = DataContext.Provider
export const DataConsumer = DataContext.Consumer
37 changes: 6 additions & 31 deletions src/data-store.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,8 @@
import { Data, Store } from './types'
import { INITIAL_ID } from './constants'
import { RawData, Key } from './types'

export function createDataStore<T>(initial: T): Store<T> {
let store = initial
let pointer = INITIAL_ID
export const defaultStore = new Map<Key, any>()

return {
init: (value) => {
pointer = INITIAL_ID
store = value
},
save: (id, value) => {
store[id] = value
},
nextId: () => {
pointer += 1
return pointer
},
resetIds: () => {
pointer = INITIAL_ID
return pointer
},
exists: (id) => store[id] !== undefined,
remove: (id) => delete store[id],
getById: (id) => (id != null ? store[id] : null),
isInitial: () => Object.keys(store).length === 0,
get: () => store,
}
}

export const defaultDataStore = createDataStore<Data>({})
export const hydrateData = (data: Data) => defaultDataStore.init(data)
export const hydrateData = (input: RawData = []) =>
input.forEach(([key, value]) => {
defaultStore.set(key, value)
})
27 changes: 3 additions & 24 deletions src/get-initial-data.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import '@testing-library/jest-dom'
import React from 'react'
import { render } from '@testing-library/react'
import { getInitialData } from './get-initial-data'
import { RUD } from './rud'
import { useRUD } from './use-rud'

jest.mock('./constants', () => ({
Expand All @@ -12,26 +11,6 @@ jest.mock('./constants', () => ({
IS_SERVER: true,
}))

test('should render response from `RUD`', async () => {
const A = ({ response }) => <div data-testid="response">{response}</div>
const B = (props) => <A {...props} />
const C = (props) => <B {...props} />

const D = () => (
<RUD fetcher={() => ({ response: 'Foo' })} id="D-comp">
{(state) => <C {...state.result} />}
</RUD>
)

const element = <D />
const data = await getInitialData(element)

const { getByTestId } = render(element)

expect(getByTestId('response')).toHaveTextContent('Foo')
expect(data['D-comp']).toEqual({ response: 'Foo' })
})

test('should render response from `useRUD`', async () => {
const A = ({ response }) => <div data-testid="response">{response}</div>

Expand All @@ -53,11 +32,11 @@ test('should render response from `useRUD`', async () => {
}

const element = <D />
const data = await getInitialData(element)
const data = new Map(await getInitialData(element))

const { getByTestId } = render(element)

expect(getByTestId('response')).toHaveTextContent('Foo Bar')
expect(data['B-comp']).toEqual({ response: 'Bar' })
expect(data['D-comp']).toEqual({ response: 'Foo' })
expect(data.get('B-comp')).toEqual({ response: 'Bar' })
expect(data.get('D-comp')).toEqual({ response: 'Foo' })
})
20 changes: 5 additions & 15 deletions src/get-initial-data.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
import prepass from 'react-ssr-prepass'
import { Store } from './types'
import { defaultDataStore } from './data-store'
import { defaultStore } from './data-store'

function getInitialData(
element: React.ElementType,
dataStore: Store = defaultDataStore
): Promise<ReturnType<typeof dataStore['get']>> {
dataStore.init({})
store = defaultStore
): Promise<ReturnType<typeof store['get']>> {
store.clear()

return new Promise((resolve, reject) =>
prepass(element)
.then(() => {
dataStore.resetIds() // prepare for next render
resolve(dataStore.get())
})
.catch((error: Error) => {
reject(error)
})
)
return prepass(element).then(() => Array.from(store))
}

export { getInitialData }
3 changes: 1 addition & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export { createDataStore, hydrateData } from './data-store'
export { hydrateData } from './data-store'
export { DataProvider } from './context'
export { getInitialData } from './get-initial-data'
export { RUD } from './rud'
export { useRUD } from './use-rud'
11 changes: 0 additions & 11 deletions src/rud.ts

This file was deleted.

14 changes: 1 addition & 13 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
export type Key = number | string
export type Data = Record<Key, any>

export type Store<D extends Data = Data> = {
init: (data?: D) => void
save: (id: keyof D, value: D[typeof id]) => void
getById: (id: keyof D) => D[typeof id]
get: () => D
exists: (id: keyof D) => boolean
remove: (id: keyof D) => boolean
nextId: () => number
isInitial: () => boolean
resetIds: () => void
}
export type RawData = [Key, any][]

export type AsyncState<T> =
// initial
Expand Down
22 changes: 1 addition & 21 deletions src/use-async-state.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,13 @@
import { useReducer, useMemo } from 'react'
import { AsyncState } from './types'

export const INITIAL_STATE: AsyncState<null> = {
isReady: false,
isLoading: false,
error: null,
result: null,
}

export const FINISH_STATE: AsyncState<null> = {
isReady: true,
isLoading: false,
error: null,
result: null,
}
import { ActionTypes, INITIAL_STATE, FINISH_STATE } from './constants'

type Action<T> =
| { type: ActionTypes.START }
| { type: ActionTypes.FINISH; payload: T | Error }

type Reducer<T> = (prevState: AsyncState<T>, action: Action<T>) => AsyncState<T>

const enum ActionTypes {
START = 'START',
FINISH = 'FINISH',
}

const reducer: Reducer<any> = (prevState, action) => {
switch (action.type) {
case ActionTypes.START:
Expand All @@ -37,15 +19,13 @@ const reducer: Reducer<any> = (prevState, action) => {
case ActionTypes.FINISH: {
if (action.payload instanceof Error) {
return {
...prevState,
...FINISH_STATE,
isReady: false,
error: action.payload,
}
}

return {
...prevState,
...FINISH_STATE,
result: action.payload,
}
Expand Down
32 changes: 12 additions & 20 deletions src/use-rud.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,33 @@
import { useContext, useLayoutEffect } from 'react'
import { IS_CLIENT, IS_SERVER } from './constants'
import { IS_CLIENT, IS_SERVER, FINISH_STATE } from './constants'
import { DataContext } from './context'
import { AsyncState, Key, Context, Fetcher, Store } from './types'
import { useAsyncState, INITIAL_STATE, FINISH_STATE } from './use-async-state'
import { AsyncState, Key, Context, Fetcher } from './types'
import { useAsyncState } from './use-async-state'

const CONTEXT: Context = {
isServer: IS_SERVER,
isClient: IS_CLIENT,
}
const CONTEXT: Context = { isServer: IS_SERVER, isClient: IS_CLIENT }
const finished = <T>(result: T) => Object.assign({}, FINISH_STATE, { result })

function useServerData<T>(fetcher: Fetcher<T>, id: Key): AsyncState<T> {
const store = useContext(DataContext)

if (store.exists(id)) {
const result: T = store.getById(id)
return { ...FINISH_STATE, result }
if (store.has(id)) {
return finished<T>(store.get(id))
}

const promise = Promise.resolve(fetcher(id, CONTEXT))
promise.then((result) => store.save(id, result))
promise.then((result) => store.set(id, result))

throw promise
}

function init<T>(id: Key, store: Store): AsyncState<T> {
return store.exists(id)
? { ...FINISH_STATE, result: store.getById(id) }
: { ...INITIAL_STATE }
}

function useClientData<T>(fetcher: Fetcher<T>, id: Key): AsyncState<T> {
const store = useContext(DataContext)
const [state, actions] = useAsyncState<T>(init<T>(id, store))
const init = store.has(id) ? finished<T>(store.get(id)) : null
const [state, actions] = useAsyncState<T>(init)

useLayoutEffect(() => {
if (store.exists(id)) {
store.remove(id)
if (store.has(id)) {
store.delete(id)
return
}

Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"compilerOptions": {
"rootDir": "./src",
"target": "es5",
"lib": ["es2017", "es6", "dom"],
"lib": ["es6", "dom"],
"moduleResolution": "node",
"strict": true,
"strictNullChecks": false,
Expand Down

0 comments on commit 7b05bf1

Please sign in to comment.