Skip to content

Commit

Permalink
Update mirror.defaults to merge new reducers instead of replacing…
Browse files Browse the repository at this point in the history
… the previous ones
  • Loading branch information
llh911001 committed Sep 8, 2018
1 parent 76ff695 commit 4f3b1ec
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 31 deletions.
6 changes: 6 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## [v0.2.12](https://github.com/mirrorjs/mirror/compare/v0.2.11...v0.2.12)

> 2018-09-08
* Allow `mirror.defaults` to *merge* new Redux reducers into the previous ones.

## [v0.2.11](https://github.com/mirrorjs/mirror/compare/v0.2.10...v0.2.11)

> 2018-05-04
Expand Down
80 changes: 74 additions & 6 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,8 @@ countHook()

`mirror.defaults` is a pretty intuitive API, you use it to configure your Mirror app.

`mirror.defaults` can be called multiple times.

#### * `options.initialState`

* Default: `undefined`
Expand Down Expand Up @@ -470,6 +472,33 @@ mirror.defaults({
})
```

##### Update, not replace

There's something special about `options.reducers`, that is its `key-value`s will be **merged** into the previous ones instead of replacing them, since you can call `mirror.defaults` multiple times. That's the case when you call it after your app has been [started](#rendercomponent-container-callback), for example:

```js
// after this call, your store will hava a standard reducer with namespace
// of `a`
mirror.defaults({
reducers: {
// standard Redux reducer
a: (state, data) => {}
}
})

// ...

// then somewhere in your app, you can add other standard Redux reducers
mirror.defaults({
reducers: {
// standard Redux reducer
b: (state, data) => {}
}
})
```

After the second call, your store will have 2 reducers: `a` and `b`.

#### * `options.addEffect`

* Default: `(effects) => (name, handler) => { effects[name] = handler }`
Expand All @@ -492,12 +521,15 @@ It first creates your `Redux` store, then renders your component to DOM using `R

You can call `render` multiple times in your app. The first time being called, `render` will create a `Redux` store using all the `reducers` and `effects` you defined through `mirror.model` method. After that, all later calls will [replace your store's reducer](http://redux.js.org/docs/api/Store.html#replaceReducer) and re-render your app.

What's the point of that? It allows you to inject models dynamically.
What's the point of that? It allows you to inject models dynamically, it's very convenient for code-splitting.

#### Update models on the fly

For example, suppose you have an `app.js`:

```js
// app.js

import React from 'react'
import mirror, {actions, connect, render} from 'mirrorx'

Expand Down Expand Up @@ -529,19 +561,31 @@ After `render`, your app will be rendered as:
</div>
```

Then, somewhere in you app, you define a model `bar`, or load it from a remote server:
Then, suppose you have an async component/model which can be loaded by tools like [react-loadable](https://github.com/jamiebuilds/react-loadable):

```js
// ...
// asyncComponent.js

// inject an async model, this will not trigger the re-render
// inside this async component, you define an "async model"
mirror.model({
name: 'bar',
initialState: 'state of bar'
})
```


```js
// app.js

// this will do the re-render
render()
// ...

// some where in your app, after loading above component and model,
// call `render()` will "register" the async model and re-render your app.
//
// NOTE: the `load` function is NOT a real implementation, it's just psuedo code.
load('ayncComponent.js').then(() => {
mirror.render()
})
```

**Calling `render` without arguments will re-render your app**. So above code will generate the following `html`:
Expand All @@ -554,6 +598,30 @@ render()
</div>
```

#### Update standard reducers on the fly

Plus, after the "async component/model" has been loaded, it's possible to call `mirorr.defaults` to add some standard Redux reducers on the fly:


```js
// app.js

// NOTE: the `load` function is NOT a real implementation, it's just psuedo code.
load('ayncComponent.js').then(() => {

// `MyAsyncReducer` will be **merged** into the existed ones, not replace them
mirror.defaults({
reducers: {
MyAsyncReducer: (state, data) => {},
// ...
}
})

// do the re-render
mirror.render()
})
```

This is very useful for large apps.

> Note: it's not recommended to pass `component` and `container` to re-render your app, because React may unmount/mount your app. If you just want to re-render, call `render` without any arguments.
Expand Down
75 changes: 69 additions & 6 deletions docs/zh/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,33 @@ mirror.defaults({
})
```

##### 更新,而不是替换

`mirror.defautls` 可以调用多次,那么在后续的调用中,`options.reducers` 对象是被**更新**的,而不是被替换。也就是说,参数 `options.reducers` 中的 `key-value` 会被**合并**到之前的对象上去。例如:

```js
// 首次调用,store 中会有一个标准的 reducer 其命名空间为 `a`
mirror.defaults({
reducers: {
// standard Redux reducer
a: (state, data) => {}
}
})

// ...

// 然后在 app 的某个地方,你可以动态地增加标准 reducer
mirror.defaults({
reducers: {
// standard Redux reducer
b: (state, data) => {}
}
})
```

上述第二次的 `mirror.defaults` 调用,将会导致 store 中有 2 个标准 reducer:`a``b`


#### * `options.addEffect`

* Default: `(effects) => (name, handler) => { effects[name] = handler }`
Expand All @@ -497,7 +524,9 @@ Mirror 的 `render` 接口就是加强版的 [`ReactDOM.render`](https://faceboo

你可以在 app 中多次调用 `render`。第一次调用会使用 `mirror.model` 方法中定义的 reducer 和 effect 来创建 store。后续的调用将会 [使用 `replaceReducer` 替换 store 的 reducer](http://redux.js.org/docs/api/Store.html#replaceReducer),并重新渲染整个 app。

这样处理的意义是什么呢?就是你可以动态载入 model 了。
这样处理的意义是什么呢?就是你可以动态载入 model 了,这对 code-splitting 非常有用。

#### 动态加载 model

举例来说,假如你有一个 `app.js`

Expand Down Expand Up @@ -534,19 +563,30 @@ render(<App/>, document.getElementById('root'))
</div>
```

然后,你在 app 的某个地方,定义了一个 `bar` model,或者是通过 ajax 加载过来的
然后,假设你又顶一个 异步组件/model,可以通过类似 [react-loadable](https://github.com/jamiebuilds/react-loadable) 这样的库加载进来

```js
// ...
// asyncComponent.js

// 注入一个异步 model。注意这一步不会导致重新渲染
// 在这个异步组件中,定义一个"异步 model"
mirror.model({
name: 'bar',
initialState: 'state of bar'
})
```

// 调用 render 后,会重新渲染
render()
```js
// app.js

// ...

// 当加载完这个异步组件之后,调用 `render()` 将会“注册”其对应的异步 model,
// 并重新渲染 app
//
// NOTE: 这里的 `load` 函数为伪代码
load('ayncComponent.js').then(() => {
mirror.render()
})
```

**不传递参数调用 `render` 将会重新渲染你的 app**。所以上述代码将会生成以下 DOM 结构:
Expand All @@ -559,6 +599,29 @@ render()
</div>
```

#### 动态加载标准 reducer

另外,当加载完异步组件/model 之后,还可以通过调用 `mirror.defaults` 的方式更新标准的 Redux reducer:

```js
// app.js

// NOTE: 这里的 `load` 函数为伪代码
load('ayncComponent.js').then(() => {

// `MyAsyncReducer` 会被**合并**到之前指定的 reducer 中,而非替换它们
mirror.defaults({
reducers: {
MyAsyncReducer: (state, data) => {},
// ...
}
})

// 重新渲染
mirror.render()
})
```

这在大型 app 中非常有用。

> 注意:Mirror 不建议传递 `component``container` 参数来重新渲染你的 app,因为这样做可能会导致 React mount/unmount 你的 app。如果你只希望重新渲染,永远不要传递任何参数给 `render`
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mirrorx",
"version": "0.2.11",
"version": "0.2.12",
"description": "A React framework with minimal API and zero boilerplate.",
"scripts": {
"prepublishOnly": "npm run build",
Expand Down
17 changes: 16 additions & 1 deletion src/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ export const options = {

const historyModes = ['browser', 'hash', 'memory']

// Can be called multiple times, ie. after load an async component that has
// exported standard Redux `reducer`s that need to be `replaceReducer` for the
// store.
//
// After the first time called, all later calls will try to *merge* `opts.reducers`
// into the previous `options.reducers`, other keys like `historyMode` will be *updated*
// if it is provided, otherwise it will be ignored, which means the previous values will
// be kept.
export default function defaults(opts = {}) {

const {
Expand All @@ -53,6 +61,13 @@ export default function defaults(opts = {}) {
}

Object.keys(opts).forEach(key => {
options[key] = opts[key]
if (key === 'reducers') {
options[key] = {
...options[key],
...opts[key]
}
} else {
options[key] = opts[key]
}
})
}
26 changes: 9 additions & 17 deletions src/model.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,25 @@
import { resolveReducers, addActions } from './actions'

const isObject = target => Object.prototype.toString.call(target) === '[object Object]'

export const models = []

export default function model(m) {
const { name, reducers, initialState, effects } = validateModel(m)

m = validateModel(m)

const reducer = getReducer(resolveReducers(m.name, m.reducers), m.initialState)
const reducer = getReducer(resolveReducers(name, reducers), initialState)

const _model = {
name: m.name,
reducer
}
const toAdd = { name, reducer }

models.push(_model)
models.push(toAdd)

addActions(m.name, m.reducers, m.effects)
addActions(name, reducers, effects)

return _model
return toAdd
}

function validateModel(m = {}) {
const {
name,
reducers,
effects
} = m

const isObject = target => Object.prototype.toString.call(target) === '[object Object]'
const { name, reducers, effects } = m

if (!name || typeof name !== 'string') {
throw new Error(`Model name must be a valid string!`)
Expand Down
31 changes: 31 additions & 0 deletions test/defaults.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import defaults, { options } from 'defaults'

beforeEach(() => {
jest.resetModules()
})

describe('mirror.defaults', () => {

it('options should be exported', () => {
Expand Down Expand Up @@ -54,4 +58,31 @@ describe('mirror.defaults', () => {
}).not.toThrow()
})

it('should update `options.reducers` if call defaults multiple times', () => {
defaults({
reducers: {
a: () => {}
}
})
expect(Object.keys(options.reducers)).toEqual(['a'])

defaults({
reducers: {
b: () => {}
}
})
expect(Object.keys(options.reducers)).toEqual(['a', 'b'])
})

it('should ignore un-provided values for second and after calls', () => {
defaults({
reducers: {},
historyMode: 'hash'
})
expect(options.historyMode).toBe('hash')

defaults({})
expect(options.historyMode).toBe('hash')
})

})

0 comments on commit 4f3b1ec

Please sign in to comment.