diff --git a/.npmignore b/.npmignore
index 5d1dbcf..9d1c971 100644
--- a/.npmignore
+++ b/.npmignore
@@ -29,4 +29,5 @@ coverage/
.rts2_cache_umd/
# Github
-.github/
\ No newline at end of file
+.github/
+.git/
\ No newline at end of file
diff --git a/README.md b/README.md
index 736de71..aaad357 100644
--- a/README.md
+++ b/README.md
@@ -81,6 +81,7 @@ npm install react-model
- [FAQ](#faq)
- [How can I disable the console debugger?](#how-can-i-disable-the-console-debugger)
- [How can I add custom middleware](#how-can-i-add-custom-middleware)
+ - [How can I make persist models](#how-can-i-make-persist-models)
## Core Concept
@@ -655,3 +656,21 @@ export default Model(stores)
```
[⇧ back to top](#table-of-contents)
+
+#### How can I make persist models
+
+```typescript
+import { actionMiddlewares, Model } from 'react-model'
+import Example from 'models/example'
+
+const persistMiddleware: Middleware = async (context, restMiddlewares) => {
+ localStorage.setItem('__REACT_MODEL__', JSON.stringify(context.Global.State))
+ await context.next(restMiddlewares)
+}
+
+actionMiddlewares.push(persistMiddleware)
+
+Model({ Example }, JSON.parse(localStorage.getItem('__REACT_MODEL__')))
+```
+
+[⇧ back to top](#table-of-contents)
diff --git a/__test__/Model/mixed.spec.ts b/__test__/Model/mixed.spec.ts
new file mode 100644
index 0000000..7eccc03
--- /dev/null
+++ b/__test__/Model/mixed.spec.ts
@@ -0,0 +1,30 @@
+///
+import { testHook } from 'react-hooks-testing-library'
+import { NextCounter } from '..'
+import { Model } from '../../src'
+
+describe('useStore', () => {
+ test('create by single model definition', async () => {
+ let state: any
+ let actions: any
+ let count = 0
+ const Home = Model(NextCounter)
+ const { useStore, subscribe, unsubscribe } = Model({ Home })
+ testHook(() => {
+ ;[state, actions] = useStore('Home')
+ })
+ expect(state).toEqual({ count: 0 })
+ await actions.increment(3)
+ expect(state).toEqual({ count: 3 })
+ // test subscribe
+ subscribe('Home', 'increment', () => (count += 1))
+ await actions.increment(4)
+ expect(count).toBe(1)
+ expect(state.count).toBe(7)
+ // test unsubscribe
+ unsubscribe('Home', 'increment')
+ await actions.increment(3)
+ expect(state.count).toBe(10)
+ expect(count).toBe(1)
+ })
+})
diff --git a/__test__/Model/single.spec.ts b/__test__/Model/single.spec.ts
new file mode 100644
index 0000000..1572325
--- /dev/null
+++ b/__test__/Model/single.spec.ts
@@ -0,0 +1,29 @@
+///
+import { testHook } from 'react-hooks-testing-library'
+import { NextCounter } from '..'
+import { Model } from '../../src'
+
+describe('useStore', () => {
+ test('create by single model definition', async () => {
+ let state: any
+ let actions: any
+ let count = 0
+ const { useStore, subscribe, unsubscribe } = Model(NextCounter)
+ testHook(() => {
+ ;[state, actions] = useStore()
+ })
+ expect(state).toEqual({ count: 0 })
+ await actions.increment(3)
+ expect(state).toEqual({ count: 3 })
+ // test subscribe
+ subscribe('increment', () => (count += 1))
+ await actions.increment(4)
+ expect(count).toBe(1)
+ expect(state.count).toBe(7)
+ // test unsubscribe
+ unsubscribe('increment')
+ await actions.increment(3)
+ expect(state.count).toBe(10)
+ expect(count).toBe(1)
+ })
+})
diff --git a/__test__/class/class.spec.tsx b/__test__/class/class.spec.tsx
index 0c2cd7c..86b5d7f 100644
--- a/__test__/class/class.spec.tsx
+++ b/__test__/class/class.spec.tsx
@@ -4,7 +4,6 @@ import * as React from 'react'
import { Model, Provider, connect } from '../../src'
import { Counter } from '../index'
import { render, fireEvent } from 'react-testing-library'
-import { testHook } from 'react-hooks-testing-library'
import { timeout } from '../../src/helper'
const Button = connect(
@@ -51,22 +50,4 @@ describe('class component', () => {
await timeout(100, {}) // Wait Consumer rerender
expect(button!.textContent).toBe('3')
})
- test('communicator', async () => {
- let state: any
- const { useStore } = Model({ Counter })
- testHook(() => {
- ;[state] = useStore('Counter')
- })
- const { container } = render(
-
-
-
- )
- const button: any = container.firstChild
- expect(button!.textContent).toBe('0')
- fireEvent.click(button)
- await timeout(100, {}) // Wait Consumer rerender
- expect(button!.textContent).toBe('3')
- expect(state.count).toBe(3)
- })
})
diff --git a/__test__/class/communicator.spec.tsx b/__test__/class/communicator.spec.tsx
new file mode 100644
index 0000000..7a9946b
--- /dev/null
+++ b/__test__/class/communicator.spec.tsx
@@ -0,0 +1,49 @@
+///
+import 'react-testing-library/cleanup-after-each'
+import * as React from 'react'
+import { Model, Provider, connect } from '../../src'
+import { Counter } from '../index'
+import { render, fireEvent } from 'react-testing-library'
+import { testHook } from 'react-hooks-testing-library'
+import { timeout } from '../../src/helper'
+
+const Button = connect(
+ 'Counter',
+ (props: any) => props
+)(
+ class extends React.PureComponent {
+ render() {
+ const { state, actions } = this.props
+ return (
+
+ )
+ }
+ }
+)
+
+describe('class component', () => {
+ test('communicator', async () => {
+ let state: any
+ const { useStore } = Model({ Counter })
+ testHook(() => {
+ ;[state] = useStore('Counter')
+ })
+ const { container } = render(
+
+
+
+ )
+ const button: any = container.firstChild
+ expect(button!.textContent).toBe('0')
+ fireEvent.click(button)
+ await timeout(100, {}) // Wait Consumer rerender
+ expect(button!.textContent).toBe('3')
+ expect(state.count).toBe(3)
+ })
+})
diff --git a/__test__/getActions.spec.ts b/__test__/getActions.spec.ts
index 7c024b8..7d1b7aa 100644
--- a/__test__/getActions.spec.ts
+++ b/__test__/getActions.spec.ts
@@ -1,7 +1,7 @@
///
import { testHook } from 'react-hooks-testing-library'
+import { Counter } from '.'
import { Model } from '../src'
-import { Counter, AsyncCounter } from '.'
describe('useStore', () => {
test('return default initial values', () => {
@@ -25,22 +25,4 @@ describe('useStore', () => {
await actions.increment(4)
expect(state.count).toBe(7)
})
- test('consumer actions return Partial', async () => {
- let state: any
- let actions: any
- const { useStore, getActions } = Model({ Counter })
- testHook(() => {
- ;[state] = useStore('Counter')
- actions = getActions('Counter')
- })
- await actions.add(3)
- expect(state).toEqual({ count: 3 })
- })
- test('use initialModels', async () => {
- const { getInitialState } = Model({ AsyncCounter })
- const initialModels = await getInitialState()
- const { getState } = Model({ AsyncCounter }, initialModels)
- const state = getState('AsyncCounter')
- expect(state.count).toBe(1)
- })
})
diff --git a/__test__/index.ts b/__test__/index.ts
index 8ec778e..5b7f9f9 100644
--- a/__test__/index.ts
+++ b/__test__/index.ts
@@ -45,6 +45,26 @@ export const Counter: ModelType<
}
}
+// v3.0
+export const NextCounter: NextModelType<
+ CounterState,
+ CounterActionParams & ExtraActionParams
+> = {
+ state: { count: 0 },
+ actions: {
+ increment: params => {
+ return state => {
+ state.count += params
+ }
+ },
+ add: (params, { state }) => {
+ return {
+ count: state.count + params
+ }
+ }
+ }
+}
+
export const Theme: ModelType = {
state: {
theme: 'dark'
diff --git a/__test__/useStore.spec.ts b/__test__/useStore.spec.ts
deleted file mode 100644
index 686affc..0000000
--- a/__test__/useStore.spec.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-///
-import { testHook } from 'react-hooks-testing-library'
-import { Model } from '../src'
-import { Counter, AsyncCounter } from '.'
-
-describe('useStore', () => {
- test('return default initial values', () => {
- let state
- const { useStore } = Model({ Counter })
- testHook(() => {
- ;[state] = useStore('Counter')
- })
- expect(state).toEqual({ count: 0 })
- })
- test('consumer actions return function', async () => {
- let state: any
- let actions: any
- const { useStore } = Model({ Counter })
- testHook(() => {
- ;[state, actions] = useStore('Counter')
- })
- await actions.increment(3)
- expect(state).toEqual({ count: 3 })
- await actions.increment(4)
- expect(state.count).toBe(7)
- })
- test('consumer actions return Partial', async () => {
- let state: any
- let actions: any
- const { useStore } = Model({ Counter })
- testHook(() => {
- ;[state, actions] = useStore('Counter')
- })
- await actions.add(3)
- expect(state).toEqual({ count: 3 })
- })
- test('use initialModels', async () => {
- const { getInitialState } = Model({ AsyncCounter })
- const initialModels = await getInitialState()
- const { getState } = Model({ AsyncCounter }, initialModels)
- const state = getState('AsyncCounter')
- expect(state.count).toBe(1)
- })
-})
diff --git a/__test__/useStore/actions.spec.ts b/__test__/useStore/actions.spec.ts
new file mode 100644
index 0000000..093d04b
--- /dev/null
+++ b/__test__/useStore/actions.spec.ts
@@ -0,0 +1,17 @@
+///
+import { testHook } from 'react-hooks-testing-library'
+import { Counter } from '..'
+import { Model } from '../../src'
+
+describe('useStore', () => {
+ test('consumer actions return Partial', async () => {
+ let state: any
+ let actions: any
+ const { useStore } = Model({ Counter })
+ testHook(() => {
+ ;[state, actions] = useStore('Counter')
+ })
+ await actions.add(3)
+ expect(state).toEqual({ count: 3 })
+ })
+})
diff --git a/__test__/useStore/initial.spec.ts b/__test__/useStore/initial.spec.ts
new file mode 100644
index 0000000..86fb7dd
--- /dev/null
+++ b/__test__/useStore/initial.spec.ts
@@ -0,0 +1,27 @@
+///
+import { testHook } from 'react-hooks-testing-library'
+import { Model } from '../../src'
+import { Counter } from '..'
+
+describe('useStore', () => {
+ test('return default initial values', () => {
+ let state
+ const { useStore } = Model({ Counter })
+ testHook(() => {
+ ;[state] = useStore('Counter')
+ })
+ expect(state).toEqual({ count: 0 })
+ })
+ test('consumer actions return function', async () => {
+ let state: any
+ let actions: any
+ const { useStore } = Model({ Counter })
+ testHook(() => {
+ ;[state, actions] = useStore('Counter')
+ })
+ await actions.increment(3)
+ expect(state).toEqual({ count: 3 })
+ await actions.increment(4)
+ expect(state.count).toBe(7)
+ })
+})
diff --git a/__test__/useStore/initialModels.spec.ts b/__test__/useStore/initialModels.spec.ts
new file mode 100644
index 0000000..b7971d0
--- /dev/null
+++ b/__test__/useStore/initialModels.spec.ts
@@ -0,0 +1,13 @@
+///
+import { AsyncCounter } from '..'
+import { Model } from '../../src'
+
+describe('useStore', () => {
+ test('use initialModels', async () => {
+ const { getInitialState } = Model({ AsyncCounter })
+ const initialModels = await getInitialState()
+ const { getState } = Model({ AsyncCounter }, initialModels)
+ const state = getState('AsyncCounter')
+ expect(state.count).toBe(1)
+ })
+})
diff --git a/package.json b/package.json
index 88ec517..af6a914 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react-model",
- "version": "2.6.0",
+ "version": "2.7.0",
"description": "The State management library for React",
"main": "./dist/react-model.js",
"umd:main": "./dist/react-model.umd.js",
@@ -44,7 +44,7 @@
"remark-preset-lint-recommended": "^3.0.2",
"ts-jest": "^24.0.0",
"tslint": "^5.14.0",
- "typescript": "^3.3.1"
+ "typescript": "^3.3.4000"
},
"husky": {
"hooks": {
diff --git a/src/index.d.ts b/src/index.d.ts
index 245049d..4e83b33 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -47,12 +47,32 @@ type Action = (
| void
| Promise
+// v3.0 Action
+type NextAction = (
+ params: P,
+ context: {
+ state: S
+ actions: getConsumerNextActionsType>
+ }
+) =>
+ | Partial
+ | Promise>
+ | ProduceFunc
+ | Promise>
+ | void
+ | Promise
+
type ProduceFunc = (state: S) => void
type Actions = {
[P in keyof ActionKeys]: Action
}
+// v3.0 Actions
+type NextActions = {
+ [P in keyof ActionKeys]: NextAction
+}
+
type Dispatch = (value: A) => void
type SetStateAction = S | ((prevState: S) => S)
@@ -87,8 +107,67 @@ type Context = (InnerContext) & {
type Middleware = (C: Context, M: Middleware[]) => void
-interface Models {
- [name: string]: ModelType
+interface Models {
+ [name: string]:
+ | ModelType
+ | API>
+}
+
+interface API {
+ __id: string
+ useStore: (
+ depActions?: Array
+ ) => [Get, getConsumerNextActionsType>]
+ getState: () => Readonly>
+ subscribe: (
+ actionName: keyof MT['actions'] | Array,
+ callback: () => void
+ ) => void
+ unsubscribe: (
+ actionName: keyof Get | Array>
+ ) => void
+ actions: Readonly>>
+}
+
+interface APIs {
+ useStore: (
+ name: K,
+ depActions?: Array>
+ ) => M[K] extends API
+ ? ReturnType>
+ : M[K] extends ModelType
+ ? [Get, getConsumerActionsType>]
+ : any
+
+ getState: (
+ modelName: K
+ ) => M[K] extends ModelType
+ ? Readonly>
+ : M[K] extends API
+ ? ReturnType>
+ : any
+ getActions: (
+ modelName: K
+ ) => M[K] extends ModelType
+ ? Readonly>>
+ : undefined
+ getInitialState: (
+ context?: T | undefined
+ ) => Promise<{
+ [modelName: string]: any
+ }>
+ subscribe: (
+ modelName: K,
+ actionName: keyof Get | Array>,
+ callback: () => void
+ ) => void
+ unsubscribe: (
+ modelName: K,
+ actionName: keyof Get | Array>
+ ) => void
+ actions: {
+ [K in keyof M]: Readonly>>
+ }
}
type ModelType = {
@@ -99,6 +178,19 @@ type ModelType = {
asyncState?: (context?: any) => Promise>
}
+// v3.0
+type NextModelType = {
+ actions: {
+ [P in keyof ActionKeys]: NextAction<
+ InitStateType,
+ ActionKeys[P],
+ ActionKeys
+ >
+ }
+ state: InitStateType
+ asyncState?: (context?: any) => Promise>
+}
+
type ArgumentTypes = F extends (...args: infer A) => any
? A
: never
@@ -115,14 +207,25 @@ type getConsumerActionsType> = {
) => ReturnType
}
+// v3.0
+type getConsumerNextActionsType> = {
+ [P in keyof A]: ArgumentTypes[0] extends undefined
+ ? (params?: ArgumentTypes[0]) => ReturnType
+ : (params: ArgumentTypes[0]) => ReturnType
+}
+
type Get